using Ink_Canvas.Controls;
using Ink_Canvas.Helpers;
using Microsoft.Win32;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;
using Path = System.IO.Path;
namespace Ink_Canvas
{
public partial class MainWindow : Window
{
///
/// 当前选中的可操作元素
///
private FrameworkElement currentSelectedElement;
///
/// 是否正在拖动
///
private bool isDragging;
///
/// 拖动起始点
///
private Point dragStartPoint;
/// 页码侧栏当前订阅 的 PDF 视图。
private PdfEmbeddedView _pdfPageSidebarEventSource;
private bool _pdfSidebarPositionRefreshPending;
/// 为 true 时,下一次成功算出 PDF 边界后的侧栏定位使用宿主 Visual 变换(仅用于刚插入/恢复 PDF 的首帧对齐)。
private bool _pdfSidebarNextPositionUseHostTransform;
#region Image
///
/// 处理图片插入按钮点击事件
///
/// 事件发送者
/// 事件参数
///
/// - 打开文件选择对话框,选择图片文件
/// - 创建并压缩图片
/// - 设置图片属性,避免被InkCanvas选择系统处理
/// - 初始化InkCanvas选择设置
/// - 添加图片到画布
/// - 等待图片加载完成后进行后续处理
/// - 初始化TransformGroup
/// - 居中缩放图片
/// - 绑定事件处理器
/// - 提交到时间机器历史记录
/// - 插入图片后切换到选择模式并刷新浮动栏高光显示
///
private async void BtnImageInsert_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Filter = "图片与 PDF|*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.pdf|图片文件|*.jpg;*.jpeg;*.png;*.bmp;*.gif|PDF|*.pdf";
if (openFileDialog.ShowDialog() == true)
{
string filePath = openFileDialog.FileName;
FrameworkElement element = await CreateAndCompressImageAsync(filePath);
if (element != null)
{
string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff");
element.Name = timestamp;
// 设置图片属性,避免被InkCanvas选择系统处理
element.IsHitTestVisible = true;
element.Focusable = false;
// 初始化InkCanvas选择设置
InitializeInkCanvasSelectionSettings();
// 先添加到画布
inkCanvas.Children.Add(element);
// 等待图片加载完成后再进行后续处理
element.Loaded += (s, args) =>
{
Dispatcher.BeginInvoke(new Action(() =>
{
// 初始化TransformGroup
InitializeElementTransform(element);
// 居中缩放
CenterAndScaleElement(element);
// 最后绑定事件处理器
BindElementEvents(element);
if (element is PdfEmbeddedView)
_pdfSidebarNextPositionUseHostTransform = true;
SyncPdfPageSidebarWithCanvas();
LogHelper.WriteLogToFile($"图片插入完成: {element.Name}");
}), DispatcherPriority.Loaded);
};
timeMachine.CommitElementInsertHistory(element);
// 插入图片后切换到选择模式并刷新浮动栏高光显示
SetCurrentToolMode(InkCanvasEditingMode.Select);
UpdateCurrentToolMode("select");
HideSubPanels("select");
}
}
}
///
/// 初始化元素的TransformGroup
///
/// 要初始化的元素
///
/// - 创建TransformGroup
/// - 添加ScaleTransform、TranslateTransform和RotateTransform
/// - 设置元素的RenderTransform
///
private void InitializeElementTransform(FrameworkElement element)
{
var transformGroup = new TransformGroup();
transformGroup.Children.Add(new ScaleTransform(1, 1));
transformGroup.Children.Add(new TranslateTransform(0, 0));
transformGroup.Children.Add(new RotateTransform(0));
element.RenderTransform = transformGroup;
}
///
/// 绑定元素事件处理器
///
/// 要绑定事件的元素
///
/// - 绑定鼠标事件(MouseLeftButtonDown、MouseLeftButtonUp、MouseMove、MouseWheel)
/// - 启用触摸操作
/// - 绑定触摸事件(ManipulationDelta、ManipulationCompleted)
/// - 设置光标为手形
/// - 禁用InkCanvas对图片的选择处理
///
private void BindElementEvents(FrameworkElement element)
{
// 鼠标事件
element.MouseLeftButtonDown += Element_MouseLeftButtonDown;
element.MouseLeftButtonUp += Element_MouseLeftButtonUp;
element.MouseMove += Element_MouseMove;
element.MouseWheel += Element_MouseWheel;
// 触摸事件
element.IsManipulationEnabled = true;
element.ManipulationDelta += Element_ManipulationDelta;
element.ManipulationCompleted += Element_ManipulationCompleted;
// 设置光标
element.Cursor = Cursors.Hand;
// 禁用InkCanvas对图片的选择处理
element.IsHitTestVisible = true;
element.Focusable = false;
}
///
/// 处理元素鼠标左键按下事件
///
/// 事件发送者
/// 事件参数
///
/// - 检查编辑模式是否为选择模式
/// - 取消之前选中的元素
/// - 选中当前元素
/// - 开始拖动
/// - 捕获鼠标
/// - 设置光标为全尺寸光标
///
private void Element_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (sender is FrameworkElement element)
{
if (inkCanvas.EditingMode != InkCanvasEditingMode.Select)
{
e.Handled = false;
return;
}
// 取消之前选中的元素
if (currentSelectedElement != null && currentSelectedElement != element)
{
// 保存当前编辑模式
var previousEditingMode = inkCanvas.EditingMode;
UnselectElement(currentSelectedElement);
// 恢复编辑模式
inkCanvas.EditingMode = previousEditingMode;
}
// 选中当前元素
SelectElement(element);
// 开始拖动
isDragging = true;
dragStartPoint = e.GetPosition(inkCanvas);
element.CaptureMouse();
element.Cursor = Cursors.SizeAll;
e.Handled = true;
}
}
///
/// 处理元素鼠标左键释放事件
///
/// 事件发送者
/// 事件参数
///
/// - 停止拖动
/// - 释放鼠标捕获
/// - 恢复光标为手形
///
private void Element_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (sender is FrameworkElement element)
{
isDragging = false;
element.ReleaseMouseCapture();
element.Cursor = Cursors.Hand;
e.Handled = true;
}
}
///
/// 处理元素触摸释放事件
///
/// 事件发送者
/// 事件参数
///
/// - 停止拖动
/// - 释放触摸捕获
/// - 恢复光标为手形
///
private void Element_TouchUp(object sender, TouchEventArgs e)
{
if (sender is FrameworkElement element)
{
isDragging = false;
element.ReleaseTouchCapture(e.TouchDevice);
element.Cursor = Cursors.Hand;
e.Handled = true;
}
}
///
/// 处理元素鼠标移动事件
///
/// 事件发送者
/// 事件参数
///
/// - 检查是否正在拖动且鼠标已捕获
/// - 获取当前鼠标位置
/// - 应用鼠标拖动变换
/// - 如果是图片元素,更新工具栏位置
/// - 如果是图片元素,更新选择点位置
/// - 更新拖动起始点
///
private void Element_MouseMove(object sender, MouseEventArgs e)
{
if (sender is FrameworkElement element && isDragging && element.IsMouseCaptured)
{
var currentPoint = e.GetPosition(inkCanvas);
// 使用鼠标拖动的完整实现机制
ApplyMouseDragTransform(element, currentPoint, dragStartPoint);
// 如果是图片元素,更新工具栏位置
if (IsBitmapLikeCanvasElement(element) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
{
UpdateImageSelectionToolbarPosition(element);
}
// 如果是图片元素,更新选择点位置
if (IsBitmapLikeCanvasElement(element) && ImageSelectionOverlay?.Visibility == Visibility.Visible)
{
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
}
dragStartPoint = currentPoint;
e.Handled = true;
}
}
///
/// 处理元素鼠标滚轮事件 - 缩放
///
/// 事件发送者
/// 事件参数
///
/// - 应用滚轮缩放变换
/// - 如果是图片元素,更新工具栏位置
/// - 如果是图片元素,更新选择点位置
///
private void Element_MouseWheel(object sender, MouseWheelEventArgs e)
{
if (sender is FrameworkElement element)
{
// 使用滚轮缩放的核心机制
ApplyWheelScaleTransform(element, e);
// 如果是图片元素,更新工具栏位置
if (IsBitmapLikeCanvasElement(element) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
{
UpdateImageSelectionToolbarPosition(element);
}
// 如果是图片元素,更新选择点位置
if (IsBitmapLikeCanvasElement(element) && ImageSelectionOverlay?.Visibility == Visibility.Visible)
{
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
}
e.Handled = true;
}
}
///
/// 处理元素触摸按下事件
///
/// 事件发送者
/// 事件参数
///
/// - 检查编辑模式是否为选择模式
/// - 取消之前选中的元素
/// - 选中当前元素
/// - 开始拖动
/// - 捕获触摸
/// - 设置光标为全尺寸光标
///
private void Element_TouchDown(object sender, TouchEventArgs e)
{
if (sender is FrameworkElement element)
{
if (inkCanvas.EditingMode != InkCanvasEditingMode.Select)
{
e.Handled = false;
return;
}
// 取消之前选中的元素
if (currentSelectedElement != null && currentSelectedElement != element)
{
// 保存当前编辑模式
var previousEditingMode = inkCanvas.EditingMode;
UnselectElement(currentSelectedElement);
// 恢复编辑模式
inkCanvas.EditingMode = previousEditingMode;
}
// 选中当前元素
SelectElement(element);
// 开始拖动
isDragging = true;
dragStartPoint = e.GetTouchPoint(inkCanvas).Position;
element.CaptureTouch(e.TouchDevice);
element.Cursor = Cursors.SizeAll;
e.Handled = true;
}
}
///
/// 处理元素触摸操作事件
///
/// 事件发送者
/// 事件参数
///
/// - 检查是否是双指手势
/// - 双指手势时,让画布级别的手势处理
/// - 单指手势时,应用触摸拖动变换
/// - 如果是图片元素,更新工具栏位置
/// - 如果是图片元素,更新选择点位置
///
private void Element_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
if (sender is FrameworkElement element)
{
// 检查是否是双指手势
if (e.Manipulators.Count() >= 2)
{
// 双指手势时,不处理单个元素的手势,让画布级别的手势处理
// 这样可以实现图片与墨迹的同步移动
e.Handled = false;
return;
}
// 单指手势时,使用触摸拖动的完整实现
ApplyTouchManipulationTransform(element, e);
// 如果是图片元素,更新工具栏位置
if (IsBitmapLikeCanvasElement(element) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
{
UpdateImageSelectionToolbarPosition(element);
}
// 如果是图片元素,更新选择点位置
if (IsBitmapLikeCanvasElement(element) && ImageSelectionOverlay?.Visibility == Visibility.Visible)
{
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
}
e.Handled = true;
}
}
///
/// 处理元素触摸操作完成事件
///
/// 事件发送者
/// 事件参数
///
/// - 可以在这里添加操作完成后的处理逻辑
///
private void Element_ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
{
// 可以在这里添加操作完成后的处理逻辑
}
///
/// 应用平移变换到元素
///
/// 要变换的元素
/// X轴偏移量
/// Y轴偏移量
///
/// - 获取元素的TransformGroup
/// - 查找TranslateTransform
/// - 应用平移变换
///
private void ApplyTranslateTransform(FrameworkElement element, double deltaX, double deltaY)
{
if (element.RenderTransform is TransformGroup transformGroup)
{
var translateTransform = transformGroup.Children.OfType().FirstOrDefault();
if (translateTransform != null)
{
translateTransform.X += deltaX;
translateTransform.Y += deltaY;
}
}
}
///
/// 应用缩放变换到元素
///
/// 要变换的元素
/// 缩放因子
/// 缩放中心
///
/// - 获取元素的TransformGroup
/// - 查找ScaleTransform
/// - 设置缩放中心
/// - 应用缩放
/// - 限制缩放范围(0.1到5.0)
///
private void ApplyScaleTransform(FrameworkElement element, double scaleFactor, Point center)
{
if (element.RenderTransform is TransformGroup transformGroup)
{
var scaleTransform = transformGroup.Children.OfType().FirstOrDefault();
if (scaleTransform != null)
{
// 设置缩放中心
scaleTransform.CenterX = center.X;
scaleTransform.CenterY = center.Y;
// 应用缩放
scaleTransform.ScaleX *= scaleFactor;
scaleTransform.ScaleY *= scaleFactor;
// 限制缩放范围
scaleTransform.ScaleX = Math.Max(0.1, Math.Min(scaleTransform.ScaleX, 5.0));
scaleTransform.ScaleY = Math.Max(0.1, Math.Min(scaleTransform.ScaleY, 5.0));
}
}
}
///
/// 应用旋转变换到元素
///
/// 要变换的元素
/// 旋转角度
///
/// - 获取元素的TransformGroup
/// - 查找RotateTransform
/// - 应用旋转变换
///
private void ApplyRotateTransform(FrameworkElement element, double angle)
{
if (element.RenderTransform is TransformGroup transformGroup)
{
var scaleTransform = transformGroup.Children.OfType().FirstOrDefault();
var translateTransform = transformGroup.Children.OfType().FirstOrDefault();
var rotateTransform = transformGroup.Children.OfType().FirstOrDefault();
if (rotateTransform == null) return;
var (ox, oy, visW, visH) = GetElementVisualBox(element);
double sX = scaleTransform?.ScaleX ?? 1;
double sY = scaleTransform?.ScaleY ?? 1;
double tx = translateTransform?.X ?? 0;
double ty = translateTransform?.Y ?? 0;
// Rotate runs last in the group, so its Center is in post-scale/translate space —
// i.e. the current visual center of the element.
rotateTransform.CenterX = tx + (ox + visW / 2) * sX;
rotateTransform.CenterY = ty + (oy + visH / 2) * sY;
rotateTransform.Angle += angle;
}
}
///
/// 选中元素
///
/// 要选中的元素
///
/// - 设置当前选中元素
/// - 根据元素类型显示不同的选择工具栏
/// - 如果是图片元素,显示图片选择工具栏和缩放选择点
/// - 如果不是图片元素,隐藏图片选择工具栏和缩放选择点
/// - 确保选择框不显示,避免蓝色边框
/// - 禁用InkCanvas的选择功能,去除控制点
/// - 保持选择模式,这样用户可以直接点击墨迹来选择
///
private void SelectElement(FrameworkElement element)
{
currentSelectedElement = element;
// 根据元素类型显示不同的选择工具栏
if (IsBitmapLikeCanvasElement(element))
{
// 显示图片选择工具栏并设置位置
if (BorderImageSelectionControl != null)
{
// 计算工具栏位置(内部会同步 PDF 右侧栏位置)
UpdateImageSelectionToolbarPosition(element);
BorderImageSelectionControl.Visibility = Visibility.Visible;
}
// 显示图片缩放选择点
ShowImageResizeHandles(element);
// 墨迹选择工具栏通过GridInkCanvasSelectionCover的可见性来控制
// 不需要直接设置BorderStrokeSelectionControl.Visibility
}
else
{
// 隐藏图片选择工具栏
if (BorderImageSelectionControl != null)
{
BorderImageSelectionControl.Visibility = Visibility.Collapsed;
}
// 隐藏图片缩放选择点
HideImageResizeHandles();
// 墨迹选择工具栏通过GridInkCanvasSelectionCover的可见性来控制
// 不需要直接设置BorderStrokeSelectionControl.Visibility
}
// 确保选择框不显示,避免蓝色边框
if (GridInkCanvasSelectionCover != null)
{
GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
}
// 禁用InkCanvas的选择功能,去除控制点
if (inkCanvas != null)
{
// 清除当前选择
inkCanvas.Select(new StrokeCollection());
// 保持选择模式,这样用户可以直接点击墨迹来选择
inkCanvas.EditingMode = InkCanvasEditingMode.Select;
}
SyncPdfPageSidebarWithCanvas();
}
///
/// 取消选中元素
///
/// 要取消选中的元素
///
/// - 隐藏图片选择工具栏
/// - 隐藏图片缩放选择点
/// - 确保选择框隐藏
/// - 确保InkCanvas处于选择模式,这样用户可以直接点击墨迹来选择
///
private void UnselectElement(FrameworkElement element)
{
// 去除选中效果
// 隐藏图片选择工具栏
if (BorderImageSelectionControl != null)
{
BorderImageSelectionControl.Visibility = Visibility.Collapsed;
}
// 隐藏图片缩放选择点
HideImageResizeHandles();
// 墨迹选择工具栏通过GridInkCanvasSelectionCover的可见性来控制
// 不需要直接设置BorderStrokeSelectionControl.Visibility
// 确保选择框隐藏
if (GridInkCanvasSelectionCover != null)
{
GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
}
// 确保InkCanvas处于选择模式,这样用户可以直接点击墨迹来选择
if (inkCanvas != null)
{
inkCanvas.EditingMode = InkCanvasEditingMode.Select;
}
SyncPdfPageSidebarWithCanvas();
}
///
/// 应用矩阵变换到元素
///
/// 要变换的元素
/// 变换矩阵
///
/// - 获取元素的RenderTransform,如果不存在则创建新的TransformGroup
/// - 创建MatrixTransform
/// - 将MatrixTransform添加到TransformGroup
///
private void ApplyElementMatrixTransform(FrameworkElement element, Matrix matrix)
{
if (element.RenderTransform is TransformGroup transformGroup)
{
// 创建MatrixTransform
var matrixTransform = new MatrixTransform(matrix);
// 将MatrixTransform添加到TransformGroup
transformGroup.Children.Add(matrixTransform);
}
}
///
/// 处理滚轮缩放的核心机制
///
/// 要缩放的元素
/// 鼠标滚轮事件参数
///
/// - 根据滚轮方向确定缩放比例(向上1.1倍,向下0.9倍)
/// - 计算选中元素的中心点作为缩放中心
/// - 创建 Matrix 对象并应用 ScaleAt 变换
/// - 对选中的图片元素应用矩阵变换
/// - 对选中的笔画应用 Transform 方法
/// - 包含异常处理
///
private void ApplyWheelScaleTransform(FrameworkElement element, MouseWheelEventArgs e)
{
try
{
// 根据滚轮方向确定缩放比例(向上1.1倍,向下0.9倍)
double scaleFactor = e.Delta > 0 ? 1.1 : 0.9;
// 计算选中元素的中心点作为缩放中心
var elementCenter = new Point(element.ActualWidth / 2, element.ActualHeight / 2);
// 创建 Matrix 对象并应用 ScaleAt 变换
var matrix = new Matrix();
matrix.ScaleAt(scaleFactor, scaleFactor, elementCenter.X, elementCenter.Y);
// 对选中的图片元素调用 ApplyElementMatrixTransform
ApplyElementMatrixTransform(element, matrix);
// 对选中的笔画应用 Transform 方法(如果有选中的笔画)
var selectedStrokes = inkCanvas.GetSelectedStrokes();
foreach (var stroke in selectedStrokes)
{
stroke.Transform(matrix, false);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"滚轮缩放失败: {ex.Message}", LogHelper.LogType.Error);
}
}
///
/// 矩阵变换的完整实现
///
/// 要变换的元素
/// 变换矩阵
/// 是否保存历史记录
///
/// - 获取元素的 RenderTransform,如果不存在则创建新的 TransformGroup
/// - 保存初始变换状态用于历史记录
/// - 创建新的 TransformGroup 并添加 MatrixTransform
/// - 将新的变换组添加到现有的变换组中
/// - 如果启用了历史记录,提交变换历史
/// - 包含异常处理
///
private void ApplyMatrixTransformToElement(FrameworkElement element, Matrix matrix, bool saveHistory = true)
{
try
{
// 获取元素的 RenderTransform,如果不存在则创建新的 TransformGroup
TransformGroup transformGroup = element.RenderTransform as TransformGroup;
if (transformGroup == null)
{
transformGroup = new TransformGroup();
element.RenderTransform = transformGroup;
}
// 保存初始变换状态用于历史记录
var initialTransform = transformGroup.Clone();
// 创建新的 TransformGroup 并添加 MatrixTransform
var newTransformGroup = new TransformGroup();
newTransformGroup.Children.Add(new MatrixTransform(matrix));
// 将新的变换组添加到现有的变换组中
transformGroup.Children.Add(newTransformGroup);
// 如果启用了历史记录,提交变换历史
if (saveHistory)
{
CommitTransformHistory(element, initialTransform, transformGroup);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"矩阵变换失败: {ex.Message}", LogHelper.LogType.Error);
}
}
///
/// 鼠标拖动的完整实现机制
///
/// 要拖动的元素
/// 当前鼠标位置
/// 起始鼠标位置
///
/// - 计算鼠标移动的位移向量
/// - 创建 Matrix 对象并应用 Translate 变换
/// - 对选中的图片元素应用矩阵变换
/// - 对选中的笔画应用变换
/// - 更新选择框的位置(如果有选择框)
/// - 包含异常处理
///
private void ApplyMouseDragTransform(FrameworkElement element, Point currentPoint, Point startPoint)
{
try
{
// 计算鼠标移动的位移向量
var delta = currentPoint - startPoint;
// 创建 Matrix 对象并应用 Translate 变换
var matrix = new Matrix();
matrix.Translate(delta.X, delta.Y);
// 对选中的图片元素应用矩阵变换
ApplyMatrixTransformToElement(element, matrix, false);
// 对选中的笔画应用变换
var selectedStrokes = inkCanvas.GetSelectedStrokes();
foreach (var stroke in selectedStrokes)
{
stroke.Transform(matrix, false);
}
// 更新选择框的位置(如果有选择框)
UpdateSelectionBorderPosition(delta);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"鼠标拖动失败: {ex.Message}", LogHelper.LogType.Error);
}
}
///
/// 更新选择框位置
///
/// 位移向量
///
/// - 更新选择框位置的逻辑
/// - 更新 BorderStrokeSelectionControl 的位置
/// - 包含异常处理
///
private void UpdateSelectionBorderPosition(Vector delta)
{
try
{
// 这里可以添加更新选择框位置的逻辑
// 例如更新 BorderStrokeSelectionControl 的位置
if (BorderStrokeSelectionControl != null)
{
var currentMargin = BorderStrokeSelectionControl.Margin;
BorderStrokeSelectionControl.Margin = new Thickness(
currentMargin.Left + delta.X,
currentMargin.Top + delta.Y,
currentMargin.Right,
currentMargin.Bottom
);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新选择框位置失败: {ex.Message}", LogHelper.LogType.Error);
}
}
///
/// 提交变换历史
///
/// 变换的元素
/// 初始变换
/// 最终变换
///
/// - 提交变换历史到时间机器的逻辑
/// - 记录变换前后的状态
/// - 包含异常处理
///
private void CommitTransformHistory(FrameworkElement element, TransformGroup initialTransform, TransformGroup finalTransform)
{
try
{
// 这里可以添加提交变换历史到时间机器的逻辑
// 例如记录变换前后的状态
LogHelper.WriteLogToFile($"变换历史已记录: 元素={element.Name}");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"提交变换历史失败: {ex.Message}", LogHelper.LogType.Error);
}
}
///
/// 触摸拖动的完整实现
///
/// 要操作的元素
/// 操作事件参数
///
/// - 支持单指拖动和多指手势
/// - 可以同时进行平移、旋转和缩放
/// - 通过 ManipulationDelta 获取手势变化信息
/// - 应用平移
/// - 支持两指缩放和旋转操作
/// - 应用变换到元素
/// - 应用变换到选中的笔画
/// - 包含异常处理
///
private void ApplyTouchManipulationTransform(FrameworkElement element, ManipulationDeltaEventArgs e)
{
try
{
var md = e.DeltaManipulation;
var matrix = new Matrix();
// 支持单指拖动和多指手势
// 可以同时进行平移、旋转和缩放
// 通过 ManipulationDelta 获取手势变化信息
var translation = md.Translation;
var rotation = md.Rotation;
var scale = md.Scale;
// 应用平移
if (translation.X != 0 || translation.Y != 0)
{
matrix.Translate(translation.X, translation.Y);
}
// 支持两指缩放和旋转操作
if (e.Manipulators.Count() >= 2)
{
var center = e.ManipulationOrigin;
// 应用缩放
if (scale.X != 1.0 || scale.Y != 1.0)
{
matrix.ScaleAt(scale.X, scale.Y, center.X, center.Y);
}
// 应用旋转
if (rotation != 0)
{
matrix.RotateAt(rotation, center.X, center.Y);
}
}
// 应用变换到元素
ApplyMatrixTransformToElement(element, matrix, false);
// 应用变换到选中的笔画
var selectedStrokes = inkCanvas.GetSelectedStrokes();
foreach (var stroke in selectedStrokes)
{
stroke.Transform(matrix, false);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"触摸操作失败: {ex.Message}", LogHelper.LogType.Error);
}
}
///
/// 创建并压缩图片
///
/// 图片文件路径
/// 创建的Image对象
///
/// - 创建文件依赖目录
/// - 复制文件到依赖目录
/// - 创建BitmapImage
/// - 如果图片尺寸大于1920x1080且设置了压缩图片,则压缩图片
/// - 否则使用原始尺寸
/// - 返回创建的Image对象
///
/// 与图片选择工具栏、缩放控制点联动的画布位图类元素(普通图片或多页 PDF 嵌入)。
private static bool IsBitmapLikeCanvasElement(FrameworkElement fe)
{
return fe is Image || fe is PdfEmbeddedView;
}
private async Task CreateAndCompressImageAsync(string filePath)
{
string fileExtension = Path.GetExtension(filePath);
if (string.Equals(fileExtension, ".pdf", StringComparison.OrdinalIgnoreCase))
return await CreateAndCompressImageFromPdfAsync(filePath);
string savePath = Path.Combine(Settings.Automation.AutoSavedStrokesLocation, "File Dependency");
if (!Directory.Exists(savePath))
{
Directory.CreateDirectory(savePath);
}
string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff");
string newFilePath = Path.Combine(savePath, timestamp + fileExtension);
await Task.Run(() => File.Copy(filePath, newFilePath, true));
return await Dispatcher.InvokeAsync(() =>
{
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.UriSource = new Uri(newFilePath);
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
int width = bitmapImage.PixelWidth;
int height = bitmapImage.PixelHeight;
Image image = new Image();
// 设置拉伸模式为Fill,支持任意比例缩放
image.Stretch = Stretch.Fill;
if (isLoaded && Settings.Canvas.IsCompressPicturesUploaded && (width > 1920 || height > 1080))
{
double scaleX = 1920.0 / width;
double scaleY = 1080.0 / height;
double scale = Math.Min(scaleX, scaleY);
TransformedBitmap transformedBitmap = new TransformedBitmap(bitmapImage, new ScaleTransform(scale, scale));
image.Source = transformedBitmap;
image.Width = transformedBitmap.PixelWidth;
image.Height = transformedBitmap.PixelHeight;
}
else
{
image.Source = bitmapImage;
image.Width = width;
image.Height = height;
}
return image;
});
}
///
/// 插入完整 PDF:嵌入控件内可翻页,右下角显示页码(类似希沃白板交互)。
///
private async Task CreateAndCompressImageFromPdfAsync(string filePath)
{
try
{
string savePath = Path.Combine(Settings.Automation.AutoSavedStrokesLocation, "File Dependency");
if (!Directory.Exists(savePath))
Directory.CreateDirectory(savePath);
string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff");
string newFilePath = Path.Combine(savePath, timestamp + ".pdf");
await Task.Run(() => File.Copy(filePath, newFilePath, true));
uint pageCount = await PdfWinRtHelper.GetPageCountAsync(newFilePath);
if (pageCount == 0)
{
ShowNotification("无法打开 PDF(可能已加密、损坏或不支持)。");
return null;
}
bool compress = isLoaded && Settings.Canvas.IsCompressPicturesUploaded;
var view = new PdfEmbeddedView();
await view.InitializeAsync(newFilePath, pageCount, compress);
view.Tag = filePath;
return view;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"插入 PDF 失败: {ex.Message}", LogHelper.LogType.Error);
ShowNotification($"插入 PDF 失败: {ex.Message}");
return null;
}
}
/// 从保存的 恢复 PDF(与打开墨迹时的图片恢复流程一致,不单独写入时间轴)。
private async Task RestorePdfFromElementInfoAsync(CanvasElementInfo info)
{
if (info == null || inkCanvas == null) return;
if (!string.Equals(info.Type, "Pdf", StringComparison.OrdinalIgnoreCase)) return;
if (string.IsNullOrEmpty(info.SourcePath) || !File.Exists(info.SourcePath)) return;
try
{
uint pageCount = await PdfWinRtHelper.GetPageCountAsync(info.SourcePath);
if (pageCount == 0) return;
bool compress = isLoaded && Settings.Canvas.IsCompressPicturesUploaded;
uint initial = 0;
if (info.PdfCurrentPage.HasValue)
initial = (uint)Math.Max(0, Math.Min(info.PdfCurrentPage.Value, (int)pageCount - 1));
var view = new PdfEmbeddedView
{
Name = "pdf_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff")
};
await view.InitializeAsync(info.SourcePath, pageCount, compress, initial);
if (info.Width > 0) view.Width = info.Width;
if (info.Height > 0) view.Height = info.Height;
InkCanvas.SetLeft(view, info.Left);
InkCanvas.SetTop(view, info.Top);
InitializeElementTransform(view);
BindElementEvents(view);
inkCanvas.Children.Add(view);
_pdfSidebarNextPositionUseHostTransform = true;
SyncPdfPageSidebarWithCanvas();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"从 .elements.json 恢复 PDF 失败: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
#region Media
///
/// 处理媒体插入按钮点击事件
///
/// 事件发送者
/// 事件参数
///
/// - 打开文件选择对话框,选择媒体文件
/// - 读取媒体文件字节
/// - 创建MediaElement
/// - 居中缩放MediaElement
/// - 设置位置并添加到画布
/// - 设置LoadedBehavior和UnloadedBehavior为Manual
/// - 媒体加载完成后播放并立即暂停
/// - 提交到时间机器历史记录
///
private async void BtnMediaInsert_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Filter = "Media files (*.mp4; *.avi; *.wmv)|*.mp4;*.avi;*.wmv";
if (openFileDialog.ShowDialog() == true)
{
string filePath = openFileDialog.FileName;
byte[] mediaBytes = await Task.Run(() => File.ReadAllBytes(filePath));
MediaElement mediaElement = await CreateMediaElementAsync(filePath);
if (mediaElement != null)
{
CenterAndScaleElement(mediaElement);
InkCanvas.SetLeft(mediaElement, 0);
InkCanvas.SetTop(mediaElement, 0);
inkCanvas.Children.Add(mediaElement);
mediaElement.LoadedBehavior = MediaState.Manual;
mediaElement.UnloadedBehavior = MediaState.Manual;
mediaElement.Loaded += async (_, args) =>
{
mediaElement.Play();
await Task.Delay(100);
mediaElement.Pause();
};
timeMachine.CommitElementInsertHistory(mediaElement);
}
}
}
///
/// 创建MediaElement
///
/// 媒体文件路径
/// 创建的MediaElement对象
///
/// - 创建文件依赖目录
/// - 创建MediaElement
/// - 设置Source、名称、LoadedBehavior和UnloadedBehavior
/// - 设置宽度和高度
/// - 复制文件到依赖目录
/// - 更新Source为新文件路径
/// - 返回创建的MediaElement对象
///
private async Task CreateMediaElementAsync(string filePath)
{
string savePath = Path.Combine(Settings.Automation.AutoSavedStrokesLocation, "File Dependency");
if (!Directory.Exists(savePath))
{
Directory.CreateDirectory(savePath);
}
return await Dispatcher.InvokeAsync(() =>
{
MediaElement mediaElement = new MediaElement();
mediaElement.Source = new Uri(filePath);
string timestamp = "media_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff");
mediaElement.Name = timestamp;
mediaElement.LoadedBehavior = MediaState.Manual;
mediaElement.UnloadedBehavior = MediaState.Manual;
mediaElement.Width = 256;
mediaElement.Height = 256;
string fileExtension = Path.GetExtension(filePath);
string newFilePath = Path.Combine(savePath, mediaElement.Name + fileExtension);
File.Copy(filePath, newFilePath, true);
mediaElement.Source = new Uri(newFilePath);
return mediaElement;
});
}
#endregion
#region Image Operations
///
/// 旋转图片
///
/// 要旋转的图片
/// 旋转角度(正数为顺时针,负数为逆时针)
private void RotateImage(Image image, double angle)
{
if (image == null) return;
try
{
// 获取当前的变换
var transformGroup = image.RenderTransform as TransformGroup ?? new TransformGroup();
// 查找现有的旋转变换
RotateTransform rotateTransform = null;
foreach (Transform transform in transformGroup.Children)
{
if (transform is RotateTransform rt)
{
rotateTransform = rt;
break;
}
}
// 如果没有旋转变换,创建一个新的
if (rotateTransform == null)
{
rotateTransform = new RotateTransform();
transformGroup.Children.Add(rotateTransform);
}
// 设置旋转中心为图片中心
rotateTransform.CenterX = image.ActualWidth / 2;
rotateTransform.CenterY = image.ActualHeight / 2;
// 累加旋转角度
rotateTransform.Angle = (rotateTransform.Angle + angle) % 360;
// 应用变换
image.RenderTransform = transformGroup;
// 提交到时间机器以支持撤销
// 注意:旋转操作目前不支持撤销,因为需要更复杂的历史记录机制
}
catch (Exception ex)
{
// 记录错误但不中断程序
Debug.WriteLine($"旋转图片时发生错误: {ex.Message}");
}
}
///
/// 克隆图片
///
/// 要克隆的图片
private Image CloneImage(Image image)
{
if (image == null) return null;
try
{
// 创建图片的副本
var clonedImage = new Image
{
Source = image.Source,
Width = image.Width,
Height = image.Height,
Stretch = image.Stretch,
RenderTransform = image.RenderTransform?.Clone()
};
// 设置位置,稍微偏移以避免重叠
InkCanvas.SetLeft(clonedImage, InkCanvas.GetLeft(image) + 20);
InkCanvas.SetTop(clonedImage, InkCanvas.GetTop(image) + 20);
// 设置图片属性,避免被InkCanvas选择系统处理
clonedImage.IsHitTestVisible = true;
clonedImage.Focusable = false;
// 初始化变换
InitializeElementTransform(clonedImage);
// 绑定事件
BindElementEvents(clonedImage);
// 添加到画布
inkCanvas.Children.Add(clonedImage);
// 提交到时间机器以支持撤销
timeMachine.CommitElementInsertHistory(clonedImage);
return clonedImage;
}
catch (Exception ex)
{
// 记录错误但不中断程序
LogHelper.WriteLogToFile($"克隆图片时发生错误: {ex.Message}", LogHelper.LogType.Error);
return null;
}
}
///
/// 克隆图片到新页面
///
/// 要克隆的图片
private void CloneImageToNewBoard(Image image)
{
if (image == null) return;
try
{
// 创建图片的副本
var clonedImage = new Image
{
Source = image.Source,
Width = image.Width,
Height = image.Height,
Stretch = image.Stretch,
RenderTransform = image.RenderTransform?.Clone()
};
// 设置位置,稍微偏移以避免重叠
InkCanvas.SetLeft(clonedImage, InkCanvas.GetLeft(image) + 20);
InkCanvas.SetTop(clonedImage, InkCanvas.GetTop(image) + 20);
// 创建新页面
BtnWhiteBoardAdd_Click(null, null);
// 添加到新页面的画布
inkCanvas.Children.Add(clonedImage);
// 提交到时间机器以支持撤销
timeMachine.CommitElementInsertHistory(clonedImage);
}
catch (Exception ex)
{
// 记录错误但不中断程序
Debug.WriteLine($"克隆图片到新页面时发生错误: {ex.Message}");
}
}
///
/// 缩放图片
///
/// 要缩放的图片
/// 缩放因子(大于1为放大,小于1为缩小)
private void ScaleImage(Image image, double scaleFactor)
{
if (image == null) return;
try
{
// 获取当前的变换
var transformGroup = image.RenderTransform as TransformGroup ?? new TransformGroup();
// 查找现有的缩放变换
ScaleTransform scaleTransform = null;
foreach (Transform transform in transformGroup.Children)
{
if (transform is ScaleTransform st)
{
scaleTransform = st;
break;
}
}
// 如果没有缩放变换,创建一个新的
if (scaleTransform == null)
{
scaleTransform = new ScaleTransform();
transformGroup.Children.Add(scaleTransform);
}
// 设置缩放中心为图片中心
scaleTransform.CenterX = image.ActualWidth / 2;
scaleTransform.CenterY = image.ActualHeight / 2;
// 应用缩放因子
scaleTransform.ScaleX *= scaleFactor;
scaleTransform.ScaleY *= scaleFactor;
// 应用变换
image.RenderTransform = transformGroup;
// 提交到时间机器以支持撤销
// 注意:缩放操作目前不支持撤销,因为需要更复杂的历史记录机制
}
catch (Exception ex)
{
// 记录错误但不中断程序
Debug.WriteLine($"缩放图片时发生错误: {ex.Message}");
}
}
///
/// 删除图片
///
/// 要删除的图片
private void DeleteImage(Image image)
{
if (image == null) return;
try
{
// 从画布中移除图片
if (inkCanvas.Children.Contains(image))
{
inkCanvas.Children.Remove(image);
// 提交到时间机器以支持撤销
timeMachine.CommitElementRemoveHistory(image);
}
}
catch (Exception ex)
{
// 记录错误但不中断程序
Debug.WriteLine($"删除图片时发生错误: {ex.Message}");
}
}
#endregion
///
/// 居中并缩放元素
///
/// 要居中缩放的元素
///
/// - 确保元素已加载且有有效尺寸
/// - 如果元素尺寸无效,等待加载完成后再处理
/// - 获取画布的实际尺寸
/// - 如果画布尺寸为0,使用窗口尺寸作为备选
/// - 如果仍然为0,使用屏幕尺寸
/// - 计算最大允许尺寸(画布的70%)
/// - 获取元素的当前尺寸
/// - 计算缩放比例
/// - 如果元素本身比最大尺寸小,不进行缩放
/// - 计算新的尺寸
/// - 设置元素尺寸
/// - 计算居中位置
/// - 确保位置不为负数
/// - 设置位置
/// - 保持TransformGroup,不清除RenderTransform
/// - 只有在没有TransformGroup时才创建
/// - 包含异常处理
///
private void CenterAndScaleElement(FrameworkElement element)
{
try
{
// 确保元素已加载且有有效尺寸
if (element == null || element.ActualWidth <= 0 || element.ActualHeight <= 0)
{
// 如果元素尺寸无效,等待加载完成后再处理
element.Loaded += (sender, e) =>
{
Dispatcher.BeginInvoke(new Action(() =>
{
CenterAndScaleElement(element);
}), DispatcherPriority.Loaded);
};
return;
}
// 获取画布的实际尺寸
double canvasWidth = inkCanvas.ActualWidth;
double canvasHeight = inkCanvas.ActualHeight;
// 如果画布尺寸为0,使用窗口尺寸作为备选
if (canvasWidth <= 0 || canvasHeight <= 0)
{
canvasWidth = ActualWidth;
canvasHeight = ActualHeight;
}
// 如果仍然为0,使用屏幕尺寸
if (canvasWidth <= 0 || canvasHeight <= 0)
{
canvasWidth = SystemParameters.PrimaryScreenWidth;
canvasHeight = SystemParameters.PrimaryScreenHeight;
}
// 计算最大允许尺寸(画布的70%)
double maxWidth = canvasWidth * 0.7;
double maxHeight = canvasHeight * 0.7;
// 获取元素的当前尺寸
double elementWidth = element.ActualWidth;
double elementHeight = element.ActualHeight;
// 计算缩放比例
double scaleX = maxWidth / elementWidth;
double scaleY = maxHeight / elementHeight;
double scale = Math.Min(scaleX, scaleY);
// 如果元素本身比最大尺寸小,不进行缩放
if (scale > 1.0)
{
scale = 1.0;
}
// 计算新的尺寸
double newWidth = elementWidth * scale;
double newHeight = elementHeight * scale;
// 设置元素尺寸
element.Width = newWidth;
element.Height = newHeight;
// 计算居中位置
double centerX = (canvasWidth - newWidth) / 2;
double centerY = (canvasHeight - newHeight) / 2;
// 确保位置不为负数
centerX = Math.Max(0, centerX);
centerY = Math.Max(0, centerY);
// 设置位置
InkCanvas.SetLeft(element, centerX);
InkCanvas.SetTop(element, centerY);
// 保持TransformGroup,不清除RenderTransform
// 这样可以保持滚轮缩放和拖动功能
if (element.RenderTransform == null || element.RenderTransform == Transform.Identity)
{
// 只有在没有TransformGroup时才创建
InitializeElementTransform(element);
}
LogHelper.WriteLogToFile($"元素居中完成: 位置({centerX}, {centerY}), 尺寸({newWidth}x{newHeight})");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"元素居中失败: {ex.Message}", LogHelper.LogType.Error);
}
}
///
/// 初始化InkCanvas选择设置
///
///
/// - 清除当前选择,避免显示控制点
/// - 设置编辑模式为非选择模式
///
private void InitializeInkCanvasSelectionSettings()
{
if (inkCanvas != null)
{
// 清除当前选择,避免显示控制点
inkCanvas.Select(new StrokeCollection());
// 设置编辑模式为非选择模式
inkCanvas.EditingMode = InkCanvasEditingMode.None;
}
}
///
/// 更新图片选择工具栏位置
///
/// 图片元素
///
/// - 获取元素的实际边界(考虑变换)
/// - 计算工具栏位置(显示在图片下方)
/// - 确保工具栏不超出画布边界
/// - 设置工具栏位置
/// - 包含异常处理
///
private void UpdateImageSelectionToolbarPosition(FrameworkElement element)
{
try
{
if (BorderImageSelectionControl == null || element == null) return;
// 获取元素的实际边界(考虑变换)
Rect elementBounds = GetElementActualBounds(element);
// 计算工具栏位置(显示在图片下方)
double toolbarLeft = elementBounds.Left + (elementBounds.Width / 2) - (BorderImageSelectionControl.ActualWidth / 2);
double toolbarTop = elementBounds.Bottom + 10; // 图片下方10像素
// 确保工具栏不超出画布边界
double maxLeft = inkCanvas.ActualWidth - BorderImageSelectionControl.ActualWidth;
double maxTop = inkCanvas.ActualHeight - BorderImageSelectionControl.ActualHeight;
toolbarLeft = Math.Max(0, Math.Min(toolbarLeft, maxLeft));
toolbarTop = Math.Max(0, Math.Min(toolbarTop, maxTop));
// 设置工具栏位置
BorderImageSelectionControl.Margin = new Thickness(toolbarLeft, toolbarTop, 0, 0);
var pdfTarget = GetPdfSidebarTargetElement();
if (pdfTarget != null && BorderPdfPageSidebar != null && BorderPdfPageSidebar.Visibility == Visibility.Visible)
UpdatePdfPageSidebarPosition(pdfTarget);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新图片选择工具栏位置失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private const double PdfPageSidebarGap = 10;
///
/// 侧栏绑定的 PDF:若当前选中的是 PDF 则用该项;否则用画布上最后一个 PdfEmbeddedView。
///
private PdfEmbeddedView GetPdfSidebarTargetElement()
{
if (inkCanvas == null) return null;
var pdfs = inkCanvas.Children.OfType().ToList();
if (pdfs.Count == 0) return null;
if (currentSelectedElement is PdfEmbeddedView sel && pdfs.Contains(sel))
return sel;
return pdfs[pdfs.Count - 1];
}
private void AttachPdfPageSidebarEvents(PdfEmbeddedView pdf)
{
if (pdf == null || _pdfPageSidebarEventSource == pdf) return;
DetachPdfPageSidebarEvents();
_pdfPageSidebarEventSource = pdf;
_pdfPageSidebarEventSource.PageNavigationStateChanged += SelectedPdf_PageNavigationStateChanged;
_pdfPageSidebarEventSource.LayoutUpdated += OnPdfSidebarTargetLayoutUpdated;
}
private void DetachPdfPageSidebarEvents()
{
if (_pdfPageSidebarEventSource != null)
{
_pdfPageSidebarEventSource.PageNavigationStateChanged -= SelectedPdf_PageNavigationStateChanged;
_pdfPageSidebarEventSource.LayoutUpdated -= OnPdfSidebarTargetLayoutUpdated;
_pdfPageSidebarEventSource = null;
}
}
private void OnPdfSidebarTargetLayoutUpdated(object sender, EventArgs e)
{
if (BorderPdfPageSidebar?.Visibility != Visibility.Visible || inkCanvas == null) return;
if (!(sender is PdfEmbeddedView p) || !ReferenceEquals(_pdfPageSidebarEventSource, p)) return;
if (!inkCanvas.Children.Contains(p))
SyncPdfPageSidebarWithCanvas();
else
RequestPdfSidebarPositionRefresh();
}
/// 在下一帧合并更新侧栏位置(初始布局、翻页改尺寸后 ActualWidth 仍为 0 时尤其需要)。
private void RequestPdfSidebarPositionRefresh()
{
if (_pdfSidebarPositionRefreshPending) return;
_pdfSidebarPositionRefreshPending = true;
Dispatcher.BeginInvoke(new Action(() =>
{
_pdfSidebarPositionRefreshPending = false;
try
{
var t = GetPdfSidebarTargetElement();
if (t == null || BorderPdfPageSidebar?.Visibility != Visibility.Visible)
{
SyncPdfPageSidebarWithCanvas();
return;
}
if (!inkCanvas.Children.Contains(t))
{
SyncPdfPageSidebarWithCanvas();
return;
}
t.UpdateLayout();
inkCanvas.UpdateLayout();
UpdatePdfPageSidebarPosition(t);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"PDF 侧栏延迟定位失败: {ex.Message}", LogHelper.LogType.Warning);
}
}), DispatcherPriority.Render);
}
///
/// 画布上存在 PDF 时始终显示右侧页码栏并跟随目标 PDF;无任何 PDF 时隐藏。
///
private void SyncPdfPageSidebarWithCanvas()
{
if (BorderPdfPageSidebar == null || inkCanvas == null) return;
// 屏幕模式(已退出白板/黑板)下不显示侧栏,避免画布仍含 PDF 时栏残留在桌面上
if (currentMode == 0)
{
DetachPdfPageSidebarEvents();
BorderPdfPageSidebar.Visibility = Visibility.Collapsed;
ResetPdfSidebarToIdle();
return;
}
var pdf = GetPdfSidebarTargetElement();
if (pdf == null)
{
DetachPdfPageSidebarEvents();
BorderPdfPageSidebar.Visibility = Visibility.Collapsed;
ResetPdfSidebarToIdle();
return;
}
AttachPdfPageSidebarEvents(pdf);
BorderPdfPageSidebar.Visibility = Visibility.Visible;
UpdatePdfSidebarFromPdf(pdf);
pdf.UpdateLayout();
inkCanvas.UpdateLayout();
UpdatePdfPageSidebarPosition(pdf);
RequestPdfSidebarPositionRefresh();
Dispatcher.BeginInvoke(new Action(() =>
{
var t = GetPdfSidebarTargetElement();
if (t != null && BorderPdfPageSidebar?.Visibility == Visibility.Visible && inkCanvas.Children.Contains(t))
UpdatePdfPageSidebarPosition(t);
}), DispatcherPriority.ContextIdle);
}
///
/// 将 PDF 专用页码栏贴在当前 PDF 右侧。常态与早期实现一致:画布坐标 + Measure(Width, ∞);仅在 为 true 时用宿主 Visual 变换对齐首帧。
///
private void UpdatePdfPageSidebarPosition(FrameworkElement element)
{
try
{
if (BorderPdfPageSidebar == null || inkCanvas == null || !(element is PdfEmbeddedView pdfEl))
return;
if (!inkCanvas.Children.Contains(pdfEl))
{
SyncPdfPageSidebarWithCanvas();
return;
}
bool wantHostOnce = _pdfSidebarNextPositionUseHostTransform;
// 插入首帧:先布局再取界,便于 Transform 与墨迹一致;常态与最初 PDF 侧栏实现一致,不在此强制 UpdateLayout。
if (wantHostOnce)
pdfEl.UpdateLayout();
Rect b = GetElementActualBounds(pdfEl);
if (b.Width <= 0 || b.Height <= 0 || double.IsNaN(b.Width) || double.IsNaN(b.Height))
{
Dispatcher.BeginInvoke(new Action(() =>
{
var t = GetPdfSidebarTargetElement();
if (t is PdfEmbeddedView pe && inkCanvas.Children.Contains(pe) && BorderPdfPageSidebar?.Visibility == Visibility.Visible)
UpdatePdfPageSidebarPosition(pe);
}), DispatcherPriority.Loaded);
return;
}
Visual sidebarHost = VisualTreeHelper.GetParent(BorderPdfPageSidebar) as Visual;
double left = 0, top = 0, maxLeft = 0, maxTop = 0;
bool hostOk = false;
if (wantHostOnce && sidebarHost != null)
{
BorderPdfPageSidebar.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
double sidebarW = BorderPdfPageSidebar.DesiredSize.Width;
double sidebarH = BorderPdfPageSidebar.DesiredSize.Height;
if (sidebarW <= 0)
sidebarW = BorderPdfPageSidebar.Width;
if (sidebarH <= 0)
sidebarH = BorderPdfPageSidebar.ActualHeight;
if (sidebarH <= 0)
sidebarH = 220;
try
{
GeneralTransform inkToHost = inkCanvas.TransformToVisual(sidebarHost);
Point tl = inkToHost.Transform(new Point(b.Left, b.Top));
Point br = inkToHost.Transform(new Point(b.Right, b.Bottom));
double rightX = Math.Max(tl.X, br.X);
double midY = (tl.Y + br.Y) * 0.5;
left = rightX + PdfPageSidebarGap;
top = midY - sidebarH * 0.5;
var feHost = sidebarHost as FrameworkElement;
double hostW = feHost != null && feHost.ActualWidth > 0 ? feHost.ActualWidth : inkCanvas.ActualWidth;
double hostH = feHost != null && feHost.ActualHeight > 0 ? feHost.ActualHeight : inkCanvas.ActualHeight;
maxLeft = Math.Max(0, hostW - sidebarW);
maxTop = Math.Max(0, hostH - sidebarH);
if (left > maxLeft)
{
double leftEdge = Math.Min(tl.X, br.X);
double leftAlt = leftEdge - PdfPageSidebarGap - sidebarW;
if (leftAlt >= 0)
left = leftAlt;
}
hostOk = true;
}
catch
{
hostOk = false;
}
}
if (!hostOk)
{
// 与 ea74592「PDF 侧栏」初版一致:固定宽度测量竖向所需高度,再按墨迹边界与 inkCanvas 尺寸夹紧。
BorderPdfPageSidebar.Measure(new Size(BorderPdfPageSidebar.Width, double.PositiveInfinity));
double sidebarW = BorderPdfPageSidebar.DesiredSize.Width;
double sidebarH = BorderPdfPageSidebar.DesiredSize.Height;
if (sidebarW <= 0)
sidebarW = BorderPdfPageSidebar.Width;
if (sidebarH <= 0)
sidebarH = BorderPdfPageSidebar.ActualHeight;
if (sidebarH <= 0)
sidebarH = 220;
left = b.Right + PdfPageSidebarGap;
top = b.Top + (b.Height * 0.5) - (sidebarH * 0.5);
maxLeft = Math.Max(0, inkCanvas.ActualWidth - sidebarW);
maxTop = Math.Max(0, inkCanvas.ActualHeight - sidebarH);
if (left > maxLeft)
{
double leftAlt = b.Left - PdfPageSidebarGap - sidebarW;
if (leftAlt >= 0)
left = leftAlt;
}
}
if (wantHostOnce)
_pdfSidebarNextPositionUseHostTransform = false;
left = Math.Max(0, Math.Min(left, maxLeft));
top = Math.Max(0, Math.Min(top, maxTop));
BorderPdfPageSidebar.Margin = new Thickness(left, top, 0, 0);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新 PDF 右侧页码栏位置失败: {ex.Message}", LogHelper.LogType.Error);
}
}
///
/// 获取元素的实际边界(考虑变换)
///
/// 要获取边界的元素
/// 元素的实际边界
///
/// - 获取元素的Left和Top位置
/// - 如果值为NaN,设为0
/// - 获取元素的宽度和高度
/// - 检查是否有RenderTransform
/// - 如果有变换,使用变换后的边界
/// - 变换失败时回退到简单计算
/// - 没有变换时直接使用位置和大小
/// - 包含异常处理
/// - 回退到基本计算
///
private Rect GetElementActualBounds(FrameworkElement element)
{
try
{
var left = InkCanvas.GetLeft(element);
var top = InkCanvas.GetTop(element);
if (double.IsNaN(left)) left = 0;
if (double.IsNaN(top)) top = 0;
var width = element.ActualWidth > 0 ? element.ActualWidth : element.Width;
var height = element.ActualHeight > 0 ? element.ActualHeight : element.Height;
// 检查是否有RenderTransform
if (element.RenderTransform != null && element.RenderTransform != Transform.Identity)
{
try
{
// 如果有变换,使用变换后的边界
var transform = element.TransformToAncestor(inkCanvas);
var elementBounds = new Rect(0, 0, width, height);
var transformedBounds = transform.TransformBounds(elementBounds);
return transformedBounds;
}
catch
{
// 变换失败时回退到简单计算
return new Rect(left, top, width, height);
}
}
// 没有变换时直接使用位置和大小
return new Rect(left, top, width, height);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"获取元素实际边界失败: {ex.Message}", LogHelper.LogType.Error);
// 回退到基本计算
var left = InkCanvas.GetLeft(element);
var top = InkCanvas.GetTop(element);
if (double.IsNaN(left)) left = 0;
if (double.IsNaN(top)) top = 0;
return new Rect(left, top, element.ActualWidth, element.ActualHeight);
}
}
#region Image Selection Toolbar Event Handlers
///
/// 处理图片克隆功能
///
/// 事件发送者
/// 事件参数
///
/// - 检查当前选中元素是否为图片
/// - 创建克隆图片
/// - 添加到画布
/// - 初始化变换
/// - 绑定事件
/// - 记录历史
/// - 包含异常处理
///
private void BorderImageClone_MouseUp(object sender, MouseButtonEventArgs e)
{
try
{
if (currentSelectedElement is Image originalImage)
{
// 创建克隆图片
Image clonedImage = CloneImage(originalImage);
// 添加到画布
inkCanvas.Children.Add(clonedImage);
// 初始化变换
InitializeElementTransform(clonedImage);
// 绑定事件
BindElementEvents(clonedImage);
// 记录历史
timeMachine.CommitElementInsertHistory(clonedImage);
LogHelper.WriteLogToFile($"图片克隆完成: {clonedImage.Name}");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"图片克隆失败: {ex.Message}", LogHelper.LogType.Error);
}
}
///
/// 处理图片克隆到新页面功能
///
/// 事件发送者
/// 事件参数
///
/// - 检查当前选中元素是否为图片
/// - 创建新页面
/// - 创建克隆图片
/// - 设置图片属性,避免被InkCanvas选择系统处理
/// - 初始化变换
/// - 绑定事件
/// - 添加到新页面的画布
/// - 记录历史
/// - 包含异常处理
///
private void BorderImageCloneToNewBoard_MouseUp(object sender, MouseButtonEventArgs e)
{
try
{
if (currentSelectedElement is Image originalImage)
{
// 创建新页面
BtnWhiteBoardAdd_Click(null, null);
// 创建克隆图片(不添加到当前画布,因为已经创建了新页面)
Image clonedImage = CreateClonedImage(originalImage);
if (clonedImage != null)
{
// 设置图片属性,避免被InkCanvas选择系统处理
clonedImage.IsHitTestVisible = true;
clonedImage.Focusable = false;
// 初始化变换
InitializeElementTransform(clonedImage);
// 绑定事件
BindElementEvents(clonedImage);
// 添加到新页面的画布
inkCanvas.Children.Add(clonedImage);
// 记录历史
timeMachine.CommitElementInsertHistory(clonedImage);
LogHelper.WriteLogToFile($"图片克隆到新页面完成: {clonedImage.Name}");
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"图片克隆到新页面失败: {ex.Message}", LogHelper.LogType.Error);
}
}
///
/// 处理图片左旋转功能
///
/// 事件发送者
/// 事件参数
///
/// - 检查当前是否有选中元素
/// - 应用旋转变换(向左旋转45度)
/// - 如果是图片元素,更新工具栏位置
/// - 包含异常处理
///
private void BorderImageRotateLeft_MouseUp(object sender, MouseButtonEventArgs e)
{
try
{
if (currentSelectedElement != null)
{
ApplyRotateTransform(currentSelectedElement, -45);
// 更新工具栏位置
if (IsBitmapLikeCanvasElement(currentSelectedElement) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
{
UpdateImageSelectionToolbarPosition(currentSelectedElement);
}
LogHelper.WriteLogToFile("图片左旋转完成");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"图片左旋转失败: {ex.Message}", LogHelper.LogType.Error);
}
}
///
/// 处理图片右旋转功能
///
/// 事件发送者
/// 事件参数
///
/// - 检查当前是否有选中元素
/// - 应用旋转变换(向右旋转45度)
/// - 如果是图片元素,更新工具栏位置
/// - 包含异常处理
///
private void BorderImageRotateRight_MouseUp(object sender, MouseButtonEventArgs e)
{
try
{
if (currentSelectedElement != null)
{
ApplyRotateTransform(currentSelectedElement, 45);
// 更新工具栏位置
if (IsBitmapLikeCanvasElement(currentSelectedElement) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
{
UpdateImageSelectionToolbarPosition(currentSelectedElement);
}
LogHelper.WriteLogToFile("图片右旋转完成");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"图片右旋转失败: {ex.Message}", LogHelper.LogType.Error);
}
}
///
/// 处理图片缩放减小功能
///
/// 事件发送者
/// 事件参数
///
/// - 检查当前是否有选中元素
/// - 计算元素中心点
/// - 应用缩放变换(缩小到0.9倍)
/// - 如果是图片元素,更新工具栏位置
/// - 包含异常处理
///
private void GridImageScaleDecrease_MouseUp(object sender, MouseButtonEventArgs e)
{
try
{
if (currentSelectedElement != null)
{
var elementCenter = new Point(currentSelectedElement.ActualWidth / 2, currentSelectedElement.ActualHeight / 2);
ApplyScaleTransform(currentSelectedElement, 0.9, elementCenter);
// 更新工具栏位置
if (IsBitmapLikeCanvasElement(currentSelectedElement) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
{
UpdateImageSelectionToolbarPosition(currentSelectedElement);
}
LogHelper.WriteLogToFile("图片缩放减小完成");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"图片缩放减小失败: {ex.Message}", LogHelper.LogType.Error);
}
}
///
/// 处理图片缩放增大功能
///
/// 事件发送者
/// 事件参数
///
/// - 检查当前是否有选中元素
/// - 计算元素中心点
/// - 应用缩放变换(放大到1.1倍)
/// - 如果是图片元素,更新工具栏位置
/// - 包含异常处理
///
private void GridImageScaleIncrease_MouseUp(object sender, MouseButtonEventArgs e)
{
try
{
if (currentSelectedElement != null)
{
var elementCenter = new Point(currentSelectedElement.ActualWidth / 2, currentSelectedElement.ActualHeight / 2);
ApplyScaleTransform(currentSelectedElement, 1.1, elementCenter);
// 更新工具栏位置
if (IsBitmapLikeCanvasElement(currentSelectedElement) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
{
UpdateImageSelectionToolbarPosition(currentSelectedElement);
}
LogHelper.WriteLogToFile("图片缩放增大完成");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"图片缩放增大失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void ResetPdfSidebarToIdle()
{
if (TextBlockPdfSidebarPageLabel != null)
{
TextBlockPdfSidebarPageLabel.Text = "— / —";
TextBlockPdfSidebarPageLabel.Opacity = 0.55;
}
if (BorderPdfSidebarPagePrev != null)
{
BorderPdfSidebarPagePrev.Opacity = 0.35;
BorderPdfSidebarPagePrev.IsHitTestVisible = false;
}
if (BorderPdfSidebarPageNext != null)
{
BorderPdfSidebarPageNext.Opacity = 0.35;
BorderPdfSidebarPageNext.IsHitTestVisible = false;
}
}
private void UpdatePdfSidebarFromPdf(PdfEmbeddedView pdf)
{
if (pdf == null) return;
if (TextBlockPdfSidebarPageLabel != null)
{
TextBlockPdfSidebarPageLabel.Text = pdf.PageLabelText;
TextBlockPdfSidebarPageLabel.Opacity = 1.0;
}
bool prevOk = pdf.CanGoPrevious;
bool nextOk = pdf.CanGoNext;
if (BorderPdfSidebarPagePrev != null)
{
BorderPdfSidebarPagePrev.Opacity = prevOk ? 1.0 : 0.35;
BorderPdfSidebarPagePrev.IsHitTestVisible = prevOk;
}
if (BorderPdfSidebarPageNext != null)
{
BorderPdfSidebarPageNext.Opacity = nextOk ? 1.0 : 0.35;
BorderPdfSidebarPageNext.IsHitTestVisible = nextOk;
}
}
private void SelectedPdf_PageNavigationStateChanged(object sender, EventArgs e)
{
Dispatcher.BeginInvoke(new Action(() =>
{
if (sender is PdfEmbeddedView pdf)
{
if (!inkCanvas.Children.Contains(pdf))
{
SyncPdfPageSidebarWithCanvas();
return;
}
UpdatePdfSidebarFromPdf(pdf);
UpdatePdfPageSidebarPosition(pdf);
RequestPdfSidebarPositionRefresh();
}
if (currentSelectedElement != null && IsBitmapLikeCanvasElement(currentSelectedElement))
{
UpdateImageSelectionToolbarPosition(currentSelectedElement);
if (ImageSelectionOverlay?.Visibility == Visibility.Visible)
UpdateImageResizeHandlesPosition(GetElementActualBounds(currentSelectedElement));
}
}), DispatcherPriority.Loaded);
}
private async void BorderPdfSidebarPagePrev_MouseUp(object sender, MouseButtonEventArgs e)
{
try
{
var pdf = GetPdfSidebarTargetElement();
if (pdf != null && pdf.CanGoPrevious)
await pdf.GoToPreviousPageAsync();
SyncPdfPageSidebarWithCanvas();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"PDF 上一页失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private async void BorderPdfSidebarPageNext_MouseUp(object sender, MouseButtonEventArgs e)
{
try
{
var pdf = GetPdfSidebarTargetElement();
if (pdf != null && pdf.CanGoNext)
await pdf.GoToNextPageAsync();
SyncPdfPageSidebarWithCanvas();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"PDF 下一页失败: {ex.Message}", LogHelper.LogType.Error);
}
}
///
/// 处理图片删除功能
///
private void BorderImageDelete_MouseUp(object sender, MouseButtonEventArgs e)
{
try
{
if (currentSelectedElement != null)
{
// 保存删除前的编辑模式
var previousEditingMode = inkCanvas.EditingMode;
// 记录删除历史
timeMachine.CommitElementRemoveHistory(currentSelectedElement);
var toRemove = currentSelectedElement;
// 从画布中移除
inkCanvas.Children.Remove(toRemove);
// 清除选中状态
UnselectElement(toRemove);
currentSelectedElement = null;
// 恢复到删除前的编辑模式
inkCanvas.EditingMode = previousEditingMode;
SyncPdfPageSidebarWithCanvas();
LogHelper.WriteLogToFile($"图片删除完成,已恢复到编辑模式: {previousEditingMode}");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"图片删除失败: {ex.Message}", LogHelper.LogType.Error);
}
}
// 克隆图片的辅助方法(只创建图片,不添加到画布)
private Image CreateClonedImage(Image originalImage)
{
try
{
Image clonedImage = new Image();
// 复制图片源
if (originalImage.Source is BitmapSource bitmapSource)
{
clonedImage.Source = bitmapSource;
}
// 复制属性
clonedImage.Width = originalImage.Width;
clonedImage.Height = originalImage.Height;
clonedImage.Stretch = originalImage.Stretch;
clonedImage.StretchDirection = originalImage.StretchDirection;
// 复制位置(在新页面中居中显示)
double left = InkCanvas.GetLeft(originalImage);
double top = InkCanvas.GetTop(originalImage);
InkCanvas.SetLeft(clonedImage, left + 20); // 稍微偏移位置
InkCanvas.SetTop(clonedImage, top + 20);
// 复制变换
if (originalImage.RenderTransform is TransformGroup originalTransformGroup)
{
clonedImage.RenderTransform = originalTransformGroup.Clone();
}
// 设置名称
string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff");
clonedImage.Name = timestamp;
return clonedImage;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"克隆图片失败: {ex.Message}", LogHelper.LogType.Error);
return null;
}
}
///
/// 克隆墨迹集合
///
/// 要克隆的墨迹集合
/// 克隆后的墨迹集合
private StrokeCollection CloneStrokes(StrokeCollection strokes)
{
if (strokes == null || strokes.Count == 0) return new StrokeCollection();
try
{
// 创建墨迹集合的副本
var clonedStrokes = strokes.Clone();
// 为每个墨迹添加位置偏移以避免重叠
foreach (var stroke in clonedStrokes)
{
var offsetPoints = new StylusPointCollection();
foreach (var point in stroke.StylusPoints)
{
offsetPoints.Add(new StylusPoint(point.X + 20, point.Y + 20, point.PressureFactor));
}
stroke.StylusPoints = offsetPoints;
}
// 添加到画布
inkCanvas.Strokes.Add(clonedStrokes);
// 提交到时间机器以支持撤销
timeMachine.CommitStrokeUserInputHistory(clonedStrokes);
LogHelper.WriteLogToFile($"墨迹克隆完成: {clonedStrokes.Count} 个墨迹");
return clonedStrokes;
}
catch (Exception ex)
{
// 记录错误但不中断程序
LogHelper.WriteLogToFile($"克隆墨迹时发生错误: {ex.Message}", LogHelper.LogType.Error);
return new StrokeCollection();
}
}
///
/// 克隆墨迹集合到新页面
///
/// 要克隆的墨迹集合
private void CloneStrokesToNewBoard(StrokeCollection strokes)
{
if (strokes == null || strokes.Count == 0) return;
try
{
// 创建墨迹集合的副本
var clonedStrokes = strokes.Clone();
// 为每个墨迹添加位置偏移以避免重叠
foreach (var stroke in clonedStrokes)
{
var offsetPoints = new StylusPointCollection();
foreach (var point in stroke.StylusPoints)
{
offsetPoints.Add(new StylusPoint(point.X + 20, point.Y + 20, point.PressureFactor));
}
stroke.StylusPoints = offsetPoints;
}
// 创建新页面
BtnWhiteBoardAdd_Click(null, null);
// 添加到新页面的画布
inkCanvas.Strokes.Add(clonedStrokes);
// 提交到时间机器以支持撤销
timeMachine.CommitStrokeUserInputHistory(clonedStrokes);
LogHelper.WriteLogToFile($"墨迹克隆到新页面完成: {clonedStrokes.Count} 个墨迹");
}
catch (Exception ex)
{
// 记录错误但不中断程序
LogHelper.WriteLogToFile($"克隆墨迹到新页面时发生错误: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
#region Image Selection Overlay
private bool _imageOverlayHooked;
private FrameworkElement _overlayTrackedElement;
private void EnsureImageOverlayHooks()
{
if (_imageOverlayHooked || ImageSelectionOverlay == null) return;
ImageSelectionOverlay.CoordinateSource = inkCanvas;
ImageSelectionOverlay.ResizeDelta += ImageSelectionOverlay_ResizeDelta;
ImageSelectionOverlay.MoveDelta += ImageSelectionOverlay_MoveDelta;
ImageSelectionOverlay.RotateDelta += ImageSelectionOverlay_RotateDelta;
_imageOverlayHooked = true;
}
private void AttachOverlayTracking(FrameworkElement element)
{
if (_overlayTrackedElement == element) return;
DetachOverlayTracking();
_overlayTrackedElement = element;
if (element != null) element.LayoutUpdated += OverlayTrackedElement_LayoutUpdated;
}
private void DetachOverlayTracking()
{
if (_overlayTrackedElement != null)
{
_overlayTrackedElement.LayoutUpdated -= OverlayTrackedElement_LayoutUpdated;
_overlayTrackedElement = null;
}
}
private void OverlayTrackedElement_LayoutUpdated(object sender, EventArgs e)
{
if (ImageSelectionOverlay?.Visibility != Visibility.Visible) return;
if (currentSelectedElement == null || _overlayTrackedElement == null) return;
if (!ReferenceEquals(currentSelectedElement, _overlayTrackedElement)) return;
UpdateImageResizeHandlesPosition(default);
}
private void ShowImageResizeHandles(FrameworkElement element)
{
try
{
if (ImageSelectionOverlay == null || element == null) return;
EnsureImageOverlayHooks();
AttachOverlayTracking(element);
UpdateImageResizeHandlesPosition(default);
ImageSelectionOverlay.Visibility = Visibility.Visible;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"显示图片选中框失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void HideImageResizeHandles()
{
try
{
DetachOverlayTracking();
if (ImageSelectionOverlay != null)
ImageSelectionOverlay.Visibility = Visibility.Collapsed;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"隐藏图片选中框失败: {ex.Message}", LogHelper.LogType.Error);
}
}
// elementBounds parameter is ignored — overlay needs the UNROTATED bounds of the element,
// computed directly from the element's own state so rotation never distorts the frame.
private void UpdateImageResizeHandlesPosition(Rect elementBounds)
{
try
{
if (ImageSelectionOverlay == null || currentSelectedElement == null) return;
var element = currentSelectedElement;
var (ox, oy, visW, visH) = GetElementVisualBox(element);
if (visW <= 0 || visH <= 0) return;
double scaleX = 1, scaleY = 1, angle = 0;
if (element.RenderTransform is TransformGroup tg)
{
var st = tg.Children.OfType().FirstOrDefault();
var rt = tg.Children.OfType().FirstOrDefault();
if (st != null) { scaleX = st.ScaleX; scaleY = st.ScaleY; }
if (rt != null) angle = rt.Angle;
}
// Compute the visual center directly from the element's actual transform,
// so we don't have to model the transform chain ourselves.
Point visualCenterLocal = new Point(ox + visW / 2, oy + visH / 2);
Point centerCanvas;
try
{
var t = element.TransformToAncestor(inkCanvas);
centerCanvas = t.Transform(visualCenterLocal);
// Cancel out rotation: TransformToAncestor includes Rotate; we need pre-rotation
// center in canvas coords for the overlay (overlay applies rotation itself).
// The visual center is invariant under rotation around itself, so this is the
// same point in canvas coords either way.
}
catch
{
double left = InkCanvas.GetLeft(element); if (double.IsNaN(left)) left = 0;
double top = InkCanvas.GetTop(element); if (double.IsNaN(top)) top = 0;
double tx = 0, ty = 0;
if (element.RenderTransform is TransformGroup tg2)
{
var tt = tg2.Children.OfType().FirstOrDefault();
if (tt != null) { tx = tt.X; ty = tt.Y; }
}
centerCanvas = new Point(left + tx + (ox + visW / 2) * scaleX,
top + ty + (oy + visH / 2) * scaleY);
}
double scaledW = visW * scaleX;
double scaledH = visH * scaleY;
ImageSelectionOverlay.UpdateFrame(centerCanvas, scaledW, scaledH, angle);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新图片选中框位置失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private double GetElementRotationAngle(FrameworkElement element)
{
if (element?.RenderTransform is TransformGroup tg)
{
var rt = tg.Children.OfType().FirstOrDefault();
if (rt != null) return rt.Angle;
}
return 0;
}
// Visual box of the rendered content inside element.ActualWidth/Height,
// accounting for Stretch=Uniform letterboxing on Image elements.
// Returns (offsetX, offsetY, visibleW, visibleH) in base (unscaled) coords.
private static (double ox, double oy, double w, double h) GetElementVisualBox(FrameworkElement element)
{
double boxW = element.ActualWidth > 0 ? element.ActualWidth : element.Width;
double boxH = element.ActualHeight > 0 ? element.ActualHeight : element.Height;
if (double.IsNaN(boxW) || double.IsNaN(boxH) || boxW <= 0 || boxH <= 0)
return (0, 0, 0, 0);
if (element is Image img && img.Stretch == Stretch.Uniform && img.Source is BitmapSource bs
&& bs.PixelWidth > 0 && bs.PixelHeight > 0)
{
double srcAspect = (double)bs.PixelWidth / bs.PixelHeight;
double boxAspect = boxW / boxH;
double vW, vH;
if (srcAspect > boxAspect) { vW = boxW; vH = boxW / srcAspect; }
else { vH = boxH; vW = boxH * srcAspect; }
return ((boxW - vW) / 2, (boxH - vH) / 2, vW, vH);
}
return (0, 0, boxW, boxH);
}
// Rotate a canvas-space vector back into the element's local (unrotated) space.
private Vector CanvasVectorToLocal(Vector canvasDelta, double angleDegrees)
{
double rad = -angleDegrees * Math.PI / 180.0;
double cos = Math.Cos(rad);
double sin = Math.Sin(rad);
return new Vector(canvasDelta.X * cos - canvasDelta.Y * sin,
canvasDelta.X * sin + canvasDelta.Y * cos);
}
private void ImageSelectionOverlay_ResizeDelta(object sender, ImageResizeDeltaEventArgs e)
{
try
{
if (!IsBitmapLikeCanvasElement(currentSelectedElement)) return;
ResizeImageByCorner(currentSelectedElement, e.CanvasDelta, e.Corner, e.LockAspectRatio);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"图片缩放失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void ImageSelectionOverlay_MoveDelta(object sender, ImageMoveDeltaEventArgs e)
{
try
{
if (currentSelectedElement == null) return;
if (currentSelectedElement.RenderTransform is TransformGroup tg)
{
var tt = tg.Children.OfType().FirstOrDefault();
var rt = tg.Children.OfType().FirstOrDefault();
if (tt != null)
{
tt.X += e.CanvasDelta.X;
tt.Y += e.CanvasDelta.Y;
// Keep rotation center locked to the visual center so the element
// translates rigidly instead of swinging around an old pivot.
if (rt != null)
{
rt.CenterX += e.CanvasDelta.X;
rt.CenterY += e.CanvasDelta.Y;
}
}
}
UpdateImageResizeHandlesPosition(default);
if (BorderImageSelectionControl?.Visibility == Visibility.Visible)
UpdateImageSelectionToolbarPosition(currentSelectedElement);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"图片拖动失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void ImageSelectionOverlay_RotateDelta(object sender, ImageRotateDeltaEventArgs e)
{
try
{
if (currentSelectedElement == null) return;
ApplyRotateTransform(currentSelectedElement, e.AngleDelta);
UpdateImageResizeHandlesPosition(default);
if (BorderImageSelectionControl?.Visibility == Visibility.Visible)
UpdateImageSelectionToolbarPosition(currentSelectedElement);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"图片旋转失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void ResizeImageByCorner(FrameworkElement element, Vector canvasDelta,
ImageResizeCorner corner, bool lockAspect)
{
if (!(element.RenderTransform is TransformGroup transformGroup)) return;
var scaleTransform = transformGroup.Children.OfType().FirstOrDefault();
var translateTransform = transformGroup.Children.OfType().FirstOrDefault();
var rotateTransform = transformGroup.Children.OfType().FirstOrDefault();
if (scaleTransform == null || translateTransform == null) return;
double angle = rotateTransform?.Angle ?? 0;
var (ox, oy, visW, visH) = GetElementVisualBox(element);
if (visW <= 0 || visH <= 0) return;
double left = InkCanvas.GetLeft(element); if (double.IsNaN(left)) left = 0;
double top = InkCanvas.GetTop(element); if (double.IsNaN(top)) top = 0;
double curW = visW * scaleTransform.ScaleX;
double curH = visH * scaleTransform.ScaleY;
// Drag delta → local (unrotated) space so corner tracks cursor under any rotation.
Vector local = CanvasVectorToLocal(canvasDelta, angle);
double newW = curW, newH = curH;
double pivotFracX = 0, pivotFracY = 0; // opposite visual corner
switch (corner)
{
case ImageResizeCorner.TopLeft:
newW = curW - local.X; newH = curH - local.Y;
pivotFracX = 1; pivotFracY = 1;
break;
case ImageResizeCorner.TopRight:
newW = curW + local.X; newH = curH - local.Y;
pivotFracX = 0; pivotFracY = 1;
break;
case ImageResizeCorner.BottomLeft:
newW = curW - local.X; newH = curH + local.Y;
pivotFracX = 1; pivotFracY = 0;
break;
case ImageResizeCorner.BottomRight:
newW = curW + local.X; newH = curH + local.Y;
pivotFracX = 0; pivotFracY = 0;
break;
}
if (lockAspect && curW > 0 && curH > 0)
{
double uniform = Math.Min(newW / curW, newH / curH);
newW = curW * uniform;
newH = curH * uniform;
}
double newScaleX = newW / visW;
double newScaleY = newH / visH;
newScaleX = Math.Max(0.1, Math.Min(newScaleX, 10.0));
newScaleY = Math.Max(0.1, Math.Min(newScaleY, 10.0));
newW = visW * newScaleX;
newH = visH * newScaleY;
double tx = translateTransform.X;
double ty = translateTransform.Y;
// Visual pivot canvas position BEFORE the change.
// Visual box origin (pre-scale) is (ox, oy); after scale its top-left in post-scale
// space (before rotation/translate) is (ox*sx, oy*sy), size (visW*sx, visH*sy).
// Pivot pre-rotation = (tx + (ox + pivotFrac*visW) * sx, ty + (oy + pivotFrac*visH) * sy).
// Rotation center is the element visual center, which is the SAME pre-rotation anchor
// we derive below — so pre-rotation coord == canvas coord relative to (left, top) up
// to a rotation around that center. Because we rotate around the center and both
// before/after share the same center (we'll update it to newCenter below after shift),
// we must match canvas positions WITH rotation applied.
Point pivotBefore = ApplyCanvasTransform(ox + pivotFracX * visW, oy + pivotFracY * visH,
ox + visW / 2, oy + visH / 2,
scaleTransform.ScaleX, scaleTransform.ScaleY,
tx, ty, angle, left, top);
Point pivotAfter = ApplyCanvasTransform(ox + pivotFracX * visW, oy + pivotFracY * visH,
ox + visW / 2, oy + visH / 2,
newScaleX, newScaleY,
tx, ty, angle, left, top);
Vector canvasDrift = pivotBefore - pivotAfter;
translateTransform.X += canvasDrift.X;
translateTransform.Y += canvasDrift.Y;
scaleTransform.ScaleX = newScaleX;
scaleTransform.ScaleY = newScaleY;
// Update rotation center to the new visual center so future rotations behave.
if (rotateTransform != null)
{
rotateTransform.CenterX = translateTransform.X + (ox + visW / 2) * newScaleX;
rotateTransform.CenterY = translateTransform.Y + (oy + visH / 2) * newScaleY;
}
UpdateImageResizeHandlesPosition(default);
if (BorderImageSelectionControl?.Visibility == Visibility.Visible)
UpdateImageSelectionToolbarPosition(element);
}
// Canvas position of element-local point (lx, ly) under the given transform.
// Model: P_canvas = (left, top) + Rotate_center(S * (lx,ly) + T)
// where center = S * (cx, cy) + T, and rotation angle is angleDeg.
private static Point ApplyCanvasTransform(double lx, double ly, double cx, double cy,
double sx, double sy, double tx, double ty,
double angleDeg, double left, double top)
{
double preX = sx * lx + tx;
double preY = sy * ly + ty;
double centerX = sx * cx + tx;
double centerY = sy * cy + ty;
double rad = angleDeg * Math.PI / 180.0;
double cos = Math.Cos(rad);
double sin = Math.Sin(rad);
double relX = preX - centerX;
double relY = preY - centerY;
double rotX = relX * cos - relY * sin + centerX;
double rotY = relX * sin + relY * cos + centerY;
return new Point(left + rotX, top + rotY);
}
#endregion
}
}