Merge branch 'net6' into net462
This commit is contained in:
+1
-1
@@ -9,7 +9,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvas.PluginSdk", "InkC
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvas.Controls", "InkCanvas.Controls\InkCanvas.Controls.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvasForClass.IACoreHelper", "InkCanvasForClass.IACoreHelper\InkCanvasForClass.IACoreHelper.csproj", "{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvas.IACoreHelper", "InkCanvas.IACoreHelper\InkCanvas.IACoreHelper.csproj", "{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<UserControl x:Class="Ink_Canvas.Controls.ImageSelectionOverlay"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Top"
|
||||
Background="{x:Null}"
|
||||
IsHitTestVisible="True">
|
||||
<Canvas x:Name="OverlayRoot" Background="Transparent" IsHitTestVisible="True" ClipToBounds="False">
|
||||
<Rectangle x:Name="MoveSurface"
|
||||
Fill="Transparent"
|
||||
Cursor="SizeAll"
|
||||
IsHitTestVisible="True" />
|
||||
<Rectangle x:Name="FrameBorder"
|
||||
Stroke="#22D3A9"
|
||||
StrokeThickness="1.5"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
SnapsToDevicePixels="True" />
|
||||
|
||||
<Line x:Name="RotationLine"
|
||||
Stroke="#22D3A9"
|
||||
StrokeThickness="1.5"
|
||||
IsHitTestVisible="False"
|
||||
SnapsToDevicePixels="True" />
|
||||
|
||||
<Ellipse x:Name="RotationHandle"
|
||||
Width="14" Height="14"
|
||||
Fill="#22D3A9"
|
||||
Stroke="White"
|
||||
StrokeThickness="2"
|
||||
Cursor="Hand" />
|
||||
|
||||
<Ellipse x:Name="TopLeftHandle"
|
||||
Width="12" Height="12"
|
||||
Fill="White"
|
||||
Stroke="#22D3A9"
|
||||
StrokeThickness="1.5"
|
||||
Cursor="SizeNWSE" />
|
||||
<Ellipse x:Name="TopRightHandle"
|
||||
Width="12" Height="12"
|
||||
Fill="White"
|
||||
Stroke="#22D3A9"
|
||||
StrokeThickness="1.5"
|
||||
Cursor="SizeNESW" />
|
||||
<Ellipse x:Name="BottomLeftHandle"
|
||||
Width="12" Height="12"
|
||||
Fill="White"
|
||||
Stroke="#22D3A9"
|
||||
StrokeThickness="1.5"
|
||||
Cursor="SizeNESW" />
|
||||
<Ellipse x:Name="BottomRightHandle"
|
||||
Width="12" Height="12"
|
||||
Fill="White"
|
||||
Stroke="#22D3A9"
|
||||
StrokeThickness="1.5"
|
||||
Cursor="SizeNWSE" />
|
||||
</Canvas>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,260 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace Ink_Canvas.Controls
|
||||
{
|
||||
public enum ImageResizeCorner
|
||||
{
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomRight
|
||||
}
|
||||
|
||||
public class ImageResizeDeltaEventArgs : EventArgs
|
||||
{
|
||||
public ImageResizeCorner Corner { get; }
|
||||
public Vector CanvasDelta { get; }
|
||||
public bool LockAspectRatio { get; }
|
||||
|
||||
public ImageResizeDeltaEventArgs(ImageResizeCorner corner, Vector canvasDelta, bool lockAspect)
|
||||
{
|
||||
Corner = corner;
|
||||
CanvasDelta = canvasDelta;
|
||||
LockAspectRatio = lockAspect;
|
||||
}
|
||||
}
|
||||
|
||||
public class ImageMoveDeltaEventArgs : EventArgs
|
||||
{
|
||||
public Vector CanvasDelta { get; }
|
||||
public ImageMoveDeltaEventArgs(Vector delta) { CanvasDelta = delta; }
|
||||
}
|
||||
|
||||
public class ImageRotateDeltaEventArgs : EventArgs
|
||||
{
|
||||
public double AngleDelta { get; }
|
||||
public ImageRotateDeltaEventArgs(double angleDelta) { AngleDelta = angleDelta; }
|
||||
}
|
||||
|
||||
public partial class ImageSelectionOverlay : UserControl
|
||||
{
|
||||
private const double HandleSize = 12;
|
||||
private const double RotationHandleSize = 14;
|
||||
private const double RotationHandleOffset = 28;
|
||||
|
||||
public event EventHandler<ImageResizeDeltaEventArgs> ResizeDelta;
|
||||
public event EventHandler<ImageMoveDeltaEventArgs> MoveDelta;
|
||||
public event EventHandler<ImageRotateDeltaEventArgs> RotateDelta;
|
||||
public event EventHandler InteractionStarted;
|
||||
public event EventHandler InteractionEnded;
|
||||
|
||||
public IInputElement CoordinateSource { get; set; }
|
||||
|
||||
private Point _rotationCenterCanvas;
|
||||
private readonly RotateTransform _overlayRotation = new RotateTransform(0);
|
||||
|
||||
private bool _isResizing;
|
||||
private bool _isRotating;
|
||||
private bool _isMoving;
|
||||
private ImageResizeCorner _activeCorner;
|
||||
private Point _lastPoint;
|
||||
private double _lastRotationAngle;
|
||||
|
||||
public ImageSelectionOverlay()
|
||||
{
|
||||
InitializeComponent();
|
||||
RenderTransform = _overlayRotation;
|
||||
|
||||
TopLeftHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.TopLeft, e, TopLeftHandle);
|
||||
TopRightHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.TopRight, e, TopRightHandle);
|
||||
BottomLeftHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.BottomLeft, e, BottomLeftHandle);
|
||||
BottomRightHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.BottomRight, e, BottomRightHandle);
|
||||
|
||||
TopLeftHandle.MouseMove += ResizeMove;
|
||||
TopRightHandle.MouseMove += ResizeMove;
|
||||
BottomLeftHandle.MouseMove += ResizeMove;
|
||||
BottomRightHandle.MouseMove += ResizeMove;
|
||||
|
||||
TopLeftHandle.MouseLeftButtonUp += EndResize;
|
||||
TopRightHandle.MouseLeftButtonUp += EndResize;
|
||||
BottomLeftHandle.MouseLeftButtonUp += EndResize;
|
||||
BottomRightHandle.MouseLeftButtonUp += EndResize;
|
||||
|
||||
RotationHandle.MouseLeftButtonDown += BeginRotate;
|
||||
RotationHandle.MouseMove += RotateMove;
|
||||
RotationHandle.MouseLeftButtonUp += EndRotate;
|
||||
|
||||
MoveSurface.MouseLeftButtonDown += BeginMove;
|
||||
MoveSurface.MouseMove += MoveMove;
|
||||
MoveSurface.MouseLeftButtonUp += EndMove;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Position overlay so its logical rect (width × height) is centered at centerCanvas,
|
||||
/// then rotated by rotationAngleDegrees around that center to match the target element.
|
||||
/// </summary>
|
||||
public void UpdateFrame(Point centerCanvas, double width, double height, double rotationAngleDegrees)
|
||||
{
|
||||
if (width <= 0 || height <= 0) return;
|
||||
|
||||
_rotationCenterCanvas = centerCanvas;
|
||||
|
||||
double left = centerCanvas.X - width / 2;
|
||||
double top = centerCanvas.Y - height / 2;
|
||||
Margin = new Thickness(left, top, 0, 0);
|
||||
Width = width;
|
||||
Height = height;
|
||||
|
||||
RenderTransformOrigin = new Point(0, 0);
|
||||
_overlayRotation.Angle = rotationAngleDegrees;
|
||||
_overlayRotation.CenterX = width / 2;
|
||||
_overlayRotation.CenterY = height / 2;
|
||||
|
||||
FrameBorder.Width = width;
|
||||
FrameBorder.Height = height;
|
||||
System.Windows.Controls.Canvas.SetLeft(FrameBorder, 0);
|
||||
System.Windows.Controls.Canvas.SetTop(FrameBorder, 0);
|
||||
|
||||
MoveSurface.Width = width;
|
||||
MoveSurface.Height = height;
|
||||
System.Windows.Controls.Canvas.SetLeft(MoveSurface, 0);
|
||||
System.Windows.Controls.Canvas.SetTop(MoveSurface, 0);
|
||||
|
||||
double h = HandleSize / 2;
|
||||
System.Windows.Controls.Canvas.SetLeft(TopLeftHandle, -h);
|
||||
System.Windows.Controls.Canvas.SetTop(TopLeftHandle, -h);
|
||||
System.Windows.Controls.Canvas.SetLeft(TopRightHandle, width - h);
|
||||
System.Windows.Controls.Canvas.SetTop(TopRightHandle, -h);
|
||||
System.Windows.Controls.Canvas.SetLeft(BottomLeftHandle, -h);
|
||||
System.Windows.Controls.Canvas.SetTop(BottomLeftHandle, height - h);
|
||||
System.Windows.Controls.Canvas.SetLeft(BottomRightHandle, width - h);
|
||||
System.Windows.Controls.Canvas.SetTop(BottomRightHandle, height - h);
|
||||
|
||||
double rh = RotationHandleSize / 2;
|
||||
double midX = width / 2;
|
||||
System.Windows.Controls.Canvas.SetLeft(RotationHandle, midX - rh);
|
||||
System.Windows.Controls.Canvas.SetTop(RotationHandle, -RotationHandleOffset - rh);
|
||||
|
||||
RotationLine.X1 = midX;
|
||||
RotationLine.Y1 = 0;
|
||||
RotationLine.X2 = midX;
|
||||
RotationLine.Y2 = -RotationHandleOffset;
|
||||
}
|
||||
|
||||
private IInputElement GetSource() => CoordinateSource ?? (IInputElement)Parent;
|
||||
|
||||
private void BeginResize(ImageResizeCorner corner, MouseButtonEventArgs e, Ellipse handle)
|
||||
{
|
||||
var source = GetSource();
|
||||
if (source == null) return;
|
||||
_isResizing = true;
|
||||
_activeCorner = corner;
|
||||
_lastPoint = e.GetPosition(source);
|
||||
handle.CaptureMouse();
|
||||
InteractionStarted?.Invoke(this, EventArgs.Empty);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void ResizeMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!_isResizing || !(sender is Ellipse handle) || !handle.IsMouseCaptured) return;
|
||||
var source = GetSource();
|
||||
if (source == null) return;
|
||||
var current = e.GetPosition(source);
|
||||
var delta = current - _lastPoint;
|
||||
bool lockAspect = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
|
||||
ResizeDelta?.Invoke(this, new ImageResizeDeltaEventArgs(_activeCorner, delta, lockAspect));
|
||||
_lastPoint = current;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void EndResize(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!_isResizing) return;
|
||||
if (sender is Ellipse handle) handle.ReleaseMouseCapture();
|
||||
_isResizing = false;
|
||||
InteractionEnded?.Invoke(this, EventArgs.Empty);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void BeginRotate(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var source = GetSource();
|
||||
if (source == null) return;
|
||||
_isRotating = true;
|
||||
var p = e.GetPosition(source);
|
||||
_lastRotationAngle = AngleFromCenter(p);
|
||||
RotationHandle.CaptureMouse();
|
||||
InteractionStarted?.Invoke(this, EventArgs.Empty);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void RotateMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!_isRotating || !RotationHandle.IsMouseCaptured) return;
|
||||
var source = GetSource();
|
||||
if (source == null) return;
|
||||
var p = e.GetPosition(source);
|
||||
double angle = AngleFromCenter(p);
|
||||
double delta = angle - _lastRotationAngle;
|
||||
if (delta > 180) delta -= 360;
|
||||
else if (delta < -180) delta += 360;
|
||||
_lastRotationAngle = angle;
|
||||
RotateDelta?.Invoke(this, new ImageRotateDeltaEventArgs(delta));
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void EndRotate(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!_isRotating) return;
|
||||
RotationHandle.ReleaseMouseCapture();
|
||||
_isRotating = false;
|
||||
InteractionEnded?.Invoke(this, EventArgs.Empty);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void BeginMove(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var source = GetSource();
|
||||
if (source == null) return;
|
||||
_isMoving = true;
|
||||
_lastPoint = e.GetPosition(source);
|
||||
MoveSurface.CaptureMouse();
|
||||
InteractionStarted?.Invoke(this, EventArgs.Empty);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void MoveMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!_isMoving || !MoveSurface.IsMouseCaptured) return;
|
||||
var source = GetSource();
|
||||
if (source == null) return;
|
||||
var current = e.GetPosition(source);
|
||||
var delta = current - _lastPoint;
|
||||
_lastPoint = current;
|
||||
MoveDelta?.Invoke(this, new ImageMoveDeltaEventArgs(delta));
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void EndMove(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!_isMoving) return;
|
||||
MoveSurface.ReleaseMouseCapture();
|
||||
_isMoving = false;
|
||||
InteractionEnded?.Invoke(this, EventArgs.Empty);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private double AngleFromCenter(Point p)
|
||||
{
|
||||
double dx = p.X - _rotationCenterCanvas.X;
|
||||
double dy = p.Y - _rotationCenterCanvas.Y;
|
||||
return Math.Atan2(dy, dx) * 180.0 / Math.PI;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,10 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
internal class AnimationsHelper
|
||||
{
|
||||
private static UIElement ResolveAnimationTarget(UIElement element) => element;
|
||||
private static UIElement ResolveAnimationTarget(UIElement element)
|
||||
{
|
||||
return element;
|
||||
}
|
||||
|
||||
public static void ShowWithFadeIn(UIElement element, double duration = 0.15)
|
||||
{
|
||||
|
||||
@@ -1509,32 +1509,22 @@ namespace Ink_Canvas.Helpers
|
||||
int.TryParse(remoteParts[2], out int remoteBuild) &&
|
||||
int.TryParse(remoteParts[3], out int remoteRevision))
|
||||
{
|
||||
// 计算代数差异:主版本号差异 * 1000 + 次版本号差异 * 100 + 构建号差异 * 10 + 修订号差异
|
||||
int majorDiff = remoteMajor - localMajor;
|
||||
int minorDiff = remoteMinor - localMinor;
|
||||
int buildDiff = remoteBuild - localBuild;
|
||||
int revisionDiff = remoteRevision - localRevision;
|
||||
var localSemver = new Version(localMajor, localMinor, localBuild, localRevision);
|
||||
var remoteSemver = new Version(remoteMajor, remoteMinor, remoteBuild, remoteRevision);
|
||||
int direction = remoteSemver.CompareTo(localSemver);
|
||||
if (direction == 0) return 0;
|
||||
int sign = direction > 0 ? 1 : -1;
|
||||
|
||||
// 如果主版本号不同,则代数差异很大
|
||||
if (majorDiff != 0)
|
||||
{
|
||||
return majorDiff * 1000 + minorDiff * 100 + buildDiff * 10 + revisionDiff;
|
||||
}
|
||||
int majorDiff = Math.Abs(remoteMajor - localMajor);
|
||||
if (majorDiff != 0) return sign * (majorDiff * 1000);
|
||||
|
||||
// 如果次版本号不同,则代数差异中等
|
||||
if (minorDiff != 0)
|
||||
{
|
||||
return minorDiff * 100 + buildDiff * 10 + revisionDiff;
|
||||
}
|
||||
int minorDiff = Math.Abs(remoteMinor - localMinor);
|
||||
if (minorDiff != 0) return sign * (minorDiff * 100);
|
||||
|
||||
// 如果构建号不同,则代数差异较小
|
||||
if (buildDiff != 0)
|
||||
{
|
||||
return buildDiff * 10 + revisionDiff;
|
||||
}
|
||||
int buildDiff = Math.Abs(remoteBuild - localBuild);
|
||||
if (buildDiff != 0) return sign * (buildDiff * 10);
|
||||
|
||||
// 只有修订号不同,代数差异最小
|
||||
return revisionDiff;
|
||||
return sign * Math.Abs(remoteRevision - localRevision);
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Ink_Canvas.Helpers
|
||||
private bool _available;
|
||||
|
||||
private static string HelperExePath =>
|
||||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "InkCanvasForClass.IACoreHelper.exe");
|
||||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "InkCanvas.IACoreHelper.exe");
|
||||
|
||||
private string PipeName =>
|
||||
string.Format("ICC_IACoreHelper_{0}", Process.GetCurrentProcess().Id);
|
||||
|
||||
@@ -718,7 +718,7 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
try
|
||||
{
|
||||
if (!IsConnected || !IsInSlideShow || PPTApplication == null)
|
||||
if (!IsConnected || PPTApplication == null)
|
||||
return;
|
||||
|
||||
if (!Marshal.IsComObject(PPTApplication))
|
||||
@@ -733,7 +733,7 @@ namespace Ink_Canvas.Helpers
|
||||
if (slideShowWindow == null)
|
||||
return;
|
||||
|
||||
SlideShowBegin?.Invoke(slideShowWindow);
|
||||
OnSlideShowBegin(slideShowWindow);
|
||||
}
|
||||
catch (COMException comEx)
|
||||
{
|
||||
@@ -818,17 +818,10 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
UnbindEvents();
|
||||
|
||||
if (activePresentation != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
PresentationClose?.Invoke(activePresentation);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"触发PresentationClose事件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
// 注意:PresentationClose 只应在真正的关闭(OnPresentationBeforeClose)中抛出。
|
||||
// DisconnectFromPPT 会被重绑、热重载、瞬时 COM 失效恢复等内部路径调用,
|
||||
// 此时演示文稿仍处于打开状态,抛 PresentationClose 会让上层错误地清空缓存,
|
||||
// 而后续重连到同一个演示文稿也不会补发 PresentationOpen。
|
||||
|
||||
SafeReleaseComObject(slideShowWindow, "_pptSlideShowWindow");
|
||||
SafeReleaseComObject(activePresentation, "_pptActivePresentation");
|
||||
@@ -1217,6 +1210,15 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
PresentationClose?.Invoke(pres ?? _pptActivePresentation);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"触发PresentationClose事件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
|
||||
if (_bindingEvents && PPTApplication != null)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -12,6 +13,14 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
private const string DefaultDateTime = "yyyy-MM-dd HH-mm-ss-fff";
|
||||
|
||||
// Windows 保留设备名(不区分大小写)。这些名称无论是否带扩展名,CreateFile 都会失败。
|
||||
private static readonly HashSet<string> ReservedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"CON", "PRN", "AUX", "NUL",
|
||||
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||
};
|
||||
|
||||
public static string Render(string template, SaveFileNameContext ctx)
|
||||
{
|
||||
if (ctx == null) ctx = new SaveFileNameContext();
|
||||
@@ -51,7 +60,20 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
name = name.Replace(c, '_');
|
||||
}
|
||||
return name.Trim();
|
||||
|
||||
// Windows 禁止文件名以点号或空格结尾(会被静默截断甚至创建失败)。
|
||||
name = name.Trim().TrimEnd('.', ' ');
|
||||
|
||||
if (string.IsNullOrEmpty(name)) return name;
|
||||
|
||||
// 保留设备名:比较时忽略扩展名,命中则加下划线前缀以规避。
|
||||
var stem = Path.GetFileNameWithoutExtension(name);
|
||||
if (!string.IsNullOrEmpty(stem) && ReservedNames.Contains(stem))
|
||||
{
|
||||
name = "_" + name;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -294,7 +294,13 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!GetUserPrimaryToken(out IntPtr userToken))
|
||||
if (!GetCurrentProcessSessionId(out uint sessionId))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"UIAccess | 获取当前会话 ID 失败 (LastError={Marshal.GetLastWin32Error()})", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!GetUserPrimaryToken(sessionId, out IntPtr userToken))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"UIAccess | 获取用户令牌失败 (LastError={Marshal.GetLastWin32Error()})", LogHelper.LogType.Error);
|
||||
return false;
|
||||
@@ -506,11 +512,31 @@ namespace Ink_Canvas.Helpers
|
||||
finally { CloseHandle(hProc); }
|
||||
}
|
||||
|
||||
private static bool GetCurrentProcessSessionId(out uint sessionId)
|
||||
{
|
||||
sessionId = 0;
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, out IntPtr hSelfQuery))
|
||||
return false;
|
||||
try
|
||||
{
|
||||
IntPtr sesBuf = Marshal.AllocHGlobal(sizeof(uint));
|
||||
try
|
||||
{
|
||||
if (!GetTokenInformation(hSelfQuery, TokenSessionId, sesBuf, sizeof(uint), out _))
|
||||
return false;
|
||||
sessionId = (uint)Marshal.ReadInt32(sesBuf);
|
||||
return true;
|
||||
}
|
||||
finally { Marshal.FreeHGlobal(sesBuf); }
|
||||
}
|
||||
finally { CloseHandle(hSelfQuery); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 explorer.exe / ctfmon.exe 取得普通用户(非提升)令牌的主令牌副本,用于降权启动。
|
||||
/// 仅当当前进程为管理员时才能成功。
|
||||
/// </summary>
|
||||
private static bool GetUserPrimaryToken(out IntPtr userToken)
|
||||
private static bool GetUserPrimaryToken(uint sessionId, out IntPtr userToken)
|
||||
{
|
||||
userToken = IntPtr.Zero;
|
||||
|
||||
@@ -528,9 +554,9 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
if (string.Equals(pe.szExeFile, name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (TryDuplicateUserPrimaryToken(pe.th32ProcessID, out userToken))
|
||||
if (TryDuplicateUserPrimaryToken(pe.th32ProcessID, sessionId, out userToken))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"UIAccess | 已从 {name} (PID={pe.th32ProcessID}) 取得用户令牌");
|
||||
LogHelper.WriteLogToFile($"UIAccess | 已从 {name} (PID={pe.th32ProcessID}, Session={sessionId}) 取得用户令牌");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -543,7 +569,7 @@ namespace Ink_Canvas.Helpers
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryDuplicateUserPrimaryToken(uint pid, out IntPtr dupToken)
|
||||
private static bool TryDuplicateUserPrimaryToken(uint pid, uint sessionId, out IntPtr dupToken)
|
||||
{
|
||||
dupToken = IntPtr.Zero;
|
||||
|
||||
@@ -557,6 +583,18 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
try
|
||||
{
|
||||
// 会话隔离:拒绝来自其他登录会话(RDP / 终端服务 / 快速用户切换)的令牌,
|
||||
// 否则降权后进程会落到错误用户的桌面上下文中。
|
||||
IntPtr sesBuf = Marshal.AllocHGlobal(sizeof(uint));
|
||||
try
|
||||
{
|
||||
if (!GetTokenInformation(hToken, TokenSessionId, sesBuf, sizeof(uint), out _))
|
||||
return false;
|
||||
if ((uint)Marshal.ReadInt32(sesBuf) != sessionId)
|
||||
return false;
|
||||
}
|
||||
finally { Marshal.FreeHGlobal(sesBuf); }
|
||||
|
||||
// 仅接受非提升令牌(否则降权失败)
|
||||
IntPtr elevBuf = Marshal.AllocHGlobal(sizeof(int));
|
||||
try
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\InkCanvas.PluginSdk\InkCanvas.PluginSdk.csproj" />
|
||||
<ProjectReference Include="..\InkCanvas.Controls\InkCanvas.Controls.csproj" />
|
||||
<ProjectReference Include="..\InkCanvasForClass.IACoreHelper\InkCanvasForClass.IACoreHelper.csproj">
|
||||
<ProjectReference Include="..\InkCanvas.IACoreHelper\InkCanvas.IACoreHelper.csproj">
|
||||
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
|
||||
<Private>false</Private>
|
||||
</ProjectReference>
|
||||
|
||||
+22
-99
@@ -666,86 +666,10 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 图片缩放选择点 -->
|
||||
<Canvas Name="ImageResizeHandlesCanvas"
|
||||
Visibility="Collapsed"
|
||||
Panel.ZIndex="1000">
|
||||
<!-- 四个角控制点 -->
|
||||
<Ellipse Name="ImageTopLeftHandle"
|
||||
Width="8" Height="8"
|
||||
Fill="White"
|
||||
Stroke="#0078D4"
|
||||
StrokeThickness="1"
|
||||
Cursor="SizeNWSE"
|
||||
MouseLeftButtonDown="ImageResizeHandle_MouseLeftButtonDown"
|
||||
MouseLeftButtonUp="ImageResizeHandle_MouseLeftButtonUp"
|
||||
MouseMove="ImageResizeHandle_MouseMove" />
|
||||
<Ellipse Name="ImageTopRightHandle"
|
||||
Width="8" Height="8"
|
||||
Fill="White"
|
||||
Stroke="#0078D4"
|
||||
StrokeThickness="1"
|
||||
Cursor="SizeNESW"
|
||||
MouseLeftButtonDown="ImageResizeHandle_MouseLeftButtonDown"
|
||||
MouseLeftButtonUp="ImageResizeHandle_MouseLeftButtonUp"
|
||||
MouseMove="ImageResizeHandle_MouseMove" />
|
||||
<Ellipse Name="ImageBottomLeftHandle"
|
||||
Width="8" Height="8"
|
||||
Fill="White"
|
||||
Stroke="#0078D4"
|
||||
StrokeThickness="1"
|
||||
Cursor="SizeNESW"
|
||||
MouseLeftButtonDown="ImageResizeHandle_MouseLeftButtonDown"
|
||||
MouseLeftButtonUp="ImageResizeHandle_MouseLeftButtonUp"
|
||||
MouseMove="ImageResizeHandle_MouseMove" />
|
||||
<Ellipse Name="ImageBottomRightHandle"
|
||||
Width="8" Height="8"
|
||||
Fill="White"
|
||||
Stroke="#0078D4"
|
||||
StrokeThickness="1"
|
||||
Cursor="SizeNWSE"
|
||||
MouseLeftButtonDown="ImageResizeHandle_MouseLeftButtonDown"
|
||||
MouseLeftButtonUp="ImageResizeHandle_MouseLeftButtonUp"
|
||||
MouseMove="ImageResizeHandle_MouseMove" />
|
||||
|
||||
<!-- 四个边控制点 -->
|
||||
<Ellipse Name="ImageTopHandle"
|
||||
Width="8" Height="8"
|
||||
Fill="White"
|
||||
Stroke="#0078D4"
|
||||
StrokeThickness="1"
|
||||
Cursor="SizeNS"
|
||||
MouseLeftButtonDown="ImageResizeHandle_MouseLeftButtonDown"
|
||||
MouseLeftButtonUp="ImageResizeHandle_MouseLeftButtonUp"
|
||||
MouseMove="ImageResizeHandle_MouseMove" />
|
||||
<Ellipse Name="ImageBottomHandle"
|
||||
Width="8" Height="8"
|
||||
Fill="White"
|
||||
Stroke="#0078D4"
|
||||
StrokeThickness="1"
|
||||
Cursor="SizeNS"
|
||||
MouseLeftButtonDown="ImageResizeHandle_MouseLeftButtonDown"
|
||||
MouseLeftButtonUp="ImageResizeHandle_MouseLeftButtonUp"
|
||||
MouseMove="ImageResizeHandle_MouseMove" />
|
||||
<Ellipse Name="ImageLeftHandle"
|
||||
Width="8" Height="8"
|
||||
Fill="White"
|
||||
Stroke="#0078D4"
|
||||
StrokeThickness="1"
|
||||
Cursor="SizeWE"
|
||||
MouseLeftButtonDown="ImageResizeHandle_MouseLeftButtonDown"
|
||||
MouseLeftButtonUp="ImageResizeHandle_MouseLeftButtonUp"
|
||||
MouseMove="ImageResizeHandle_MouseMove" />
|
||||
<Ellipse Name="ImageRightHandle"
|
||||
Width="8" Height="8"
|
||||
Fill="White"
|
||||
Stroke="#0078D4"
|
||||
StrokeThickness="1"
|
||||
Cursor="SizeWE"
|
||||
MouseLeftButtonDown="ImageResizeHandle_MouseLeftButtonDown"
|
||||
MouseLeftButtonUp="ImageResizeHandle_MouseLeftButtonUp"
|
||||
MouseMove="ImageResizeHandle_MouseMove" />
|
||||
</Canvas>
|
||||
<!-- 图片选中框(边框 + 四角自由缩放 + 顶部旋转手柄) -->
|
||||
<localControls:ImageSelectionOverlay x:Name="ImageSelectionOverlay"
|
||||
Visibility="Collapsed"
|
||||
Panel.ZIndex="1000" />
|
||||
|
||||
</Grid>
|
||||
|
||||
@@ -2460,7 +2384,7 @@
|
||||
<Image Margin="0"
|
||||
Name="FloatingbarHeadIconImg"
|
||||
SnapsToDevicePixels="True"
|
||||
RenderOptions.BitmapScalingMode="Fant"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"
|
||||
Source="/Resources/Icons-png/icc.png" />
|
||||
</ikw:SimpleStackPanel>
|
||||
</Border>
|
||||
@@ -3066,21 +2990,20 @@
|
||||
CornerRadius="5" Background="{DynamicResource FloatBarBackground}" Opacity="1" BorderThickness="1"
|
||||
BorderBrush="#2563eb">
|
||||
<ikw:SimpleStackPanel Margin="-1,0,0,0">
|
||||
<Border BorderBrush="#1e3a8a" BorderThickness="0,0,0,1"
|
||||
CornerRadius="6,6,0,0" Background="#2563eb" Margin="-1,-1,-1,0"
|
||||
<Border BorderBrush="#1e3a8a" BorderThickness="0,0,0,1" Background="#2563eb" Margin="-1,-1,-1,0"
|
||||
Padding="1,1,1,0">
|
||||
<ikw:SimpleStackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Stretch">
|
||||
<TextBlock Text="{i18n:I18n Key=Tools_MoreFeaturesTitle}" Foreground="White" Padding="8,5,44,5"
|
||||
FontSize="11" FontWeight="Bold"
|
||||
TextAlignment="Center" />
|
||||
<Image Margin="0,0,0,0"
|
||||
Source="/Resources/new-icons/close-white.png"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"
|
||||
Height="16"
|
||||
Width="16" MouseDown="Border_MouseDown"
|
||||
MouseUp="CloseBordertools_MouseUp" />
|
||||
</ikw:SimpleStackPanel>
|
||||
<Grid Margin="6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="{i18n:I18n Key=Tools_MoreFeaturesTitle}"
|
||||
Foreground="White" FontWeight="Bold"/>
|
||||
<ui:FontIcon Grid.Column="1" Icon="{x:Static ui:SegoeFluentIcons.ChromeCloseContrast}"
|
||||
Foreground="White" FontSize="12"
|
||||
MouseDown="Border_MouseDown"
|
||||
MouseUp="CloseBordertools_MouseUp" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<!---->
|
||||
<ikw:SimpleStackPanel Margin="10,3,10,2" Spacing="2">
|
||||
@@ -3116,7 +3039,7 @@
|
||||
</Border>
|
||||
|
||||
<ikw:SimpleStackPanel Orientation="{Binding ElementName=StackPanelFloatingBar, Path=Orientation}">
|
||||
<Border Margin=" 4,0,0,0" x:Name="EnableTwoFingerGestureBorder" Visibility="Visible" Width="36"
|
||||
<Border Margin="2,0,0,0" x:Name="EnableTwoFingerGestureBorder" Visibility="Visible" Width="36"
|
||||
Height="36" CornerRadius="5" Background="{DynamicResource FloatBarBackground}" BorderBrush="{DynamicResource FloatBarBorderBrush}"
|
||||
BorderThickness="1">
|
||||
<ikw:SimpleStackPanel
|
||||
@@ -3261,9 +3184,9 @@
|
||||
</Canvas>
|
||||
</ikw:SimpleStackPanel>
|
||||
<!--Visibility="{Binding ElementName=BtnPPTSlideShowEnd, Path=Visibility}">-->
|
||||
<Border x:Name="BorderFloatingBarExitPPTBtn" Margin="2,0,0,0" Width="34" Height="36"
|
||||
<Border x:Name="BorderFloatingBarExitPPTBtn" Margin="2,0,0,0" Width="36" Height="36"
|
||||
MouseDown="Border_MouseDown" MouseUp="ImagePPTControlEnd_MouseUp"
|
||||
Background="{DynamicResource FloatBarBackground}" CornerRadius="4" BorderThickness="1"
|
||||
Background="{DynamicResource FloatBarBackground}" CornerRadius="5" BorderThickness="1"
|
||||
BorderBrush="{DynamicResource FloatBarBorderBrush}"
|
||||
Visibility="{Binding ElementName=BtnPPTSlideShowEnd, Path=Visibility}">
|
||||
<ikw:SimpleStackPanel Background="Transparent" Orientation="Vertical"
|
||||
@@ -3272,7 +3195,7 @@
|
||||
<Image Source="{DynamicResource QuickPanelEndSlideshowIcon}"
|
||||
RenderOptions.BitmapScalingMode="HighQuality" Width="28" Height="17"
|
||||
Margin="0,3,0,0" />
|
||||
<TextBlock Text="退出" Foreground="{DynamicResource FloatBarForeground}" FontSize="8" Margin="0,1,0,0"
|
||||
<TextBlock Text="{i18n:I18n Key=FloatingBar_ExitButton}" Foreground="{DynamicResource FloatBarForeground}" FontSize="8" Margin="0,1,0,0"
|
||||
TextAlignment="Center" />
|
||||
</ikw:SimpleStackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -1814,6 +1814,7 @@ namespace Ink_Canvas
|
||||
// 清除之前的更新状态,确保使用新通道重新检查
|
||||
AvailableLatestVersion = null;
|
||||
AvailableLatestLineGroup = null;
|
||||
AvailableLatestReleaseNotes = null;
|
||||
|
||||
// 使用当前选择的更新通道检查更新
|
||||
var (remoteVersion, lineGroup, apiReleaseNotes) = await AutoUpdateHelper.CheckForUpdates(Settings.Startup.UpdateChannel);
|
||||
|
||||
@@ -247,7 +247,9 @@ namespace Ink_Canvas
|
||||
SetTheme(ThemeDark);
|
||||
break;
|
||||
case 2:
|
||||
SetTheme(ThemeHelper.IsSystemThemeLightLegacy() ? ThemeLight : ThemeDark);
|
||||
// 与 IsCurrentThemeDark / GetEffectiveTheme / 浮动栏一致,统一读 AppsUseLightTheme,
|
||||
// 否则 SystemUsesLightTheme 与 AppsUseLightTheme 可独立取值时主题会混搭
|
||||
SetTheme(ThemeHelper.IsSystemThemeLight() ? ThemeLight : ThemeDark);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ namespace Ink_Canvas
|
||||
}
|
||||
|
||||
// 如果是图片元素,更新选择点位置
|
||||
if (IsBitmapLikeCanvasElement(element) && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
|
||||
if (IsBitmapLikeCanvasElement(element) && ImageSelectionOverlay?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
|
||||
}
|
||||
@@ -325,7 +325,7 @@ namespace Ink_Canvas
|
||||
}
|
||||
|
||||
// 如果是图片元素,更新选择点位置
|
||||
if (IsBitmapLikeCanvasElement(element) && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
|
||||
if (IsBitmapLikeCanvasElement(element) && ImageSelectionOverlay?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
|
||||
}
|
||||
@@ -415,7 +415,7 @@ namespace Ink_Canvas
|
||||
}
|
||||
|
||||
// 如果是图片元素,更新选择点位置
|
||||
if (IsBitmapLikeCanvasElement(element) && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
|
||||
if (IsBitmapLikeCanvasElement(element) && ImageSelectionOverlay?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
|
||||
}
|
||||
@@ -510,11 +510,22 @@ namespace Ink_Canvas
|
||||
{
|
||||
if (element.RenderTransform is TransformGroup transformGroup)
|
||||
{
|
||||
var scaleTransform = transformGroup.Children.OfType<ScaleTransform>().FirstOrDefault();
|
||||
var translateTransform = transformGroup.Children.OfType<TranslateTransform>().FirstOrDefault();
|
||||
var rotateTransform = transformGroup.Children.OfType<RotateTransform>().FirstOrDefault();
|
||||
if (rotateTransform != null)
|
||||
{
|
||||
rotateTransform.Angle += angle;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2176,7 +2187,7 @@ namespace Ink_Canvas
|
||||
if (currentSelectedElement != null && IsBitmapLikeCanvasElement(currentSelectedElement))
|
||||
{
|
||||
UpdateImageSelectionToolbarPosition(currentSelectedElement);
|
||||
if (ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
|
||||
if (ImageSelectionOverlay?.Visibility == Visibility.Visible)
|
||||
UpdateImageResizeHandlesPosition(GetElementActualBounds(currentSelectedElement));
|
||||
}
|
||||
}), DispatcherPriority.Loaded);
|
||||
@@ -2379,240 +2390,361 @@ namespace Ink_Canvas
|
||||
|
||||
#endregion
|
||||
|
||||
#region Image Resize Handles
|
||||
#region Image Selection Overlay
|
||||
|
||||
// 图片缩放选择点相关变量
|
||||
private bool isResizingImage = false;
|
||||
private Point imageResizeStartPoint;
|
||||
private string activeResizeHandle = "";
|
||||
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 (ImageResizeHandlesCanvas == null || element == null) return;
|
||||
|
||||
// 获取元素的实际边界
|
||||
Rect elementBounds = GetElementActualBounds(element);
|
||||
|
||||
// 设置选择点位置
|
||||
UpdateImageResizeHandlesPosition(elementBounds);
|
||||
|
||||
// 显示选择点
|
||||
ImageResizeHandlesCanvas.Visibility = Visibility.Visible;
|
||||
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);
|
||||
LogHelper.WriteLogToFile($"显示图片选中框失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏图片缩放选择点
|
||||
private void HideImageResizeHandles()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ImageResizeHandlesCanvas != null)
|
||||
{
|
||||
ImageResizeHandlesCanvas.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
DetachOverlayTracking();
|
||||
if (ImageSelectionOverlay != null)
|
||||
ImageSelectionOverlay.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"隐藏图片缩放选择点失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
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 (ImageResizeHandlesCanvas == null) return;
|
||||
if (ImageSelectionOverlay == null || currentSelectedElement == null) return;
|
||||
|
||||
ImageResizeHandlesCanvas.Margin = new Thickness(elementBounds.Left, elementBounds.Top, 0, 0);
|
||||
var element = currentSelectedElement;
|
||||
var (ox, oy, visW, visH) = GetElementVisualBox(element);
|
||||
if (visW <= 0 || visH <= 0) return;
|
||||
|
||||
// 四个角控制点
|
||||
System.Windows.Controls.Canvas.SetLeft(ImageTopLeftHandle, -4);
|
||||
System.Windows.Controls.Canvas.SetTop(ImageTopLeftHandle, -4);
|
||||
|
||||
System.Windows.Controls.Canvas.SetLeft(ImageTopRightHandle, elementBounds.Width - 4);
|
||||
System.Windows.Controls.Canvas.SetTop(ImageTopRightHandle, -4);
|
||||
|
||||
System.Windows.Controls.Canvas.SetLeft(ImageBottomLeftHandle, -4);
|
||||
System.Windows.Controls.Canvas.SetTop(ImageBottomLeftHandle, elementBounds.Height - 4);
|
||||
|
||||
System.Windows.Controls.Canvas.SetLeft(ImageBottomRightHandle, elementBounds.Width - 4);
|
||||
System.Windows.Controls.Canvas.SetTop(ImageBottomRightHandle, elementBounds.Height - 4);
|
||||
|
||||
// 四个边控制点
|
||||
System.Windows.Controls.Canvas.SetLeft(ImageTopHandle, elementBounds.Width / 2 - 4);
|
||||
System.Windows.Controls.Canvas.SetTop(ImageTopHandle, -4);
|
||||
|
||||
System.Windows.Controls.Canvas.SetLeft(ImageBottomHandle, elementBounds.Width / 2 - 4);
|
||||
System.Windows.Controls.Canvas.SetTop(ImageBottomHandle, elementBounds.Height - 4);
|
||||
|
||||
System.Windows.Controls.Canvas.SetLeft(ImageLeftHandle, -4);
|
||||
System.Windows.Controls.Canvas.SetTop(ImageLeftHandle, elementBounds.Height / 2 - 4);
|
||||
|
||||
System.Windows.Controls.Canvas.SetLeft(ImageRightHandle, elementBounds.Width - 4);
|
||||
System.Windows.Controls.Canvas.SetTop(ImageRightHandle, elementBounds.Height / 2 - 4);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"更新图片缩放选择点位置失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 图片缩放选择点鼠标按下事件
|
||||
private void ImageResizeHandle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsBitmapLikeCanvasElement(currentSelectedElement) && sender is Ellipse ellipse)
|
||||
double scaleX = 1, scaleY = 1, angle = 0;
|
||||
if (element.RenderTransform is TransformGroup tg)
|
||||
{
|
||||
isResizingImage = true;
|
||||
imageResizeStartPoint = e.GetPosition(inkCanvas);
|
||||
|
||||
// 确定是哪个控制点
|
||||
activeResizeHandle = ellipse.Name;
|
||||
|
||||
// 捕获鼠标
|
||||
ellipse.CaptureMouse();
|
||||
e.Handled = true;
|
||||
var st = tg.Children.OfType<ScaleTransform>().FirstOrDefault();
|
||||
var rt = tg.Children.OfType<RotateTransform>().FirstOrDefault();
|
||||
if (st != null) { scaleX = st.ScaleX; scaleY = st.ScaleY; }
|
||||
if (rt != null) angle = rt.Angle;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"图片缩放选择点鼠标按下事件失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 图片缩放选择点鼠标释放事件
|
||||
private void ImageResizeHandle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (isResizingImage && sender is Ellipse ellipse)
|
||||
// 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
|
||||
{
|
||||
isResizingImage = false;
|
||||
ellipse.ReleaseMouseCapture();
|
||||
activeResizeHandle = "";
|
||||
e.Handled = true;
|
||||
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 (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"图片缩放选择点鼠标释放事件失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 图片缩放选择点鼠标移动事件
|
||||
private void ImageResizeHandle_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (isResizingImage && IsBitmapLikeCanvasElement(currentSelectedElement) && sender is Ellipse ellipse)
|
||||
catch
|
||||
{
|
||||
var currentPoint = e.GetPosition(inkCanvas);
|
||||
ResizeImageByHandle(currentSelectedElement, imageResizeStartPoint, currentPoint, activeResizeHandle);
|
||||
imageResizeStartPoint = currentPoint;
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"图片缩放选择点鼠标移动事件失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 根据控制点缩放图片
|
||||
private void ResizeImageByHandle(FrameworkElement element, Point startPoint, Point currentPoint, string handleName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (element.RenderTransform is TransformGroup transformGroup)
|
||||
{
|
||||
var scaleTransform = transformGroup.Children.OfType<ScaleTransform>().FirstOrDefault();
|
||||
var translateTransform = transformGroup.Children.OfType<TranslateTransform>().FirstOrDefault();
|
||||
|
||||
if (scaleTransform == null || translateTransform == null) return;
|
||||
|
||||
// 获取图片的当前边界
|
||||
Rect currentBounds = GetElementActualBounds(element);
|
||||
double deltaX = currentPoint.X - startPoint.X;
|
||||
double deltaY = currentPoint.Y - startPoint.Y;
|
||||
|
||||
// 计算缩放比例
|
||||
double scaleX = 1.0;
|
||||
double scaleY = 1.0;
|
||||
double translateX = 0;
|
||||
double translateY = 0;
|
||||
|
||||
switch (handleName)
|
||||
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)
|
||||
{
|
||||
case "ImageTopLeftHandle":
|
||||
scaleX = (currentBounds.Width - deltaX) / currentBounds.Width;
|
||||
scaleY = (currentBounds.Height - deltaY) / currentBounds.Height;
|
||||
translateX = deltaX;
|
||||
translateY = deltaY;
|
||||
break;
|
||||
case "ImageTopRightHandle":
|
||||
scaleX = (currentBounds.Width + deltaX) / currentBounds.Width;
|
||||
scaleY = (currentBounds.Height - deltaY) / currentBounds.Height;
|
||||
translateY = deltaY;
|
||||
break;
|
||||
case "ImageBottomLeftHandle":
|
||||
scaleX = (currentBounds.Width - deltaX) / currentBounds.Width;
|
||||
scaleY = (currentBounds.Height + deltaY) / currentBounds.Height;
|
||||
translateX = deltaX;
|
||||
break;
|
||||
case "ImageBottomRightHandle":
|
||||
scaleX = (currentBounds.Width + deltaX) / currentBounds.Width;
|
||||
scaleY = (currentBounds.Height + deltaY) / currentBounds.Height;
|
||||
break;
|
||||
case "ImageTopHandle":
|
||||
scaleY = (currentBounds.Height - deltaY) / currentBounds.Height;
|
||||
translateY = deltaY;
|
||||
break;
|
||||
case "ImageBottomHandle":
|
||||
scaleY = (currentBounds.Height + deltaY) / currentBounds.Height;
|
||||
break;
|
||||
case "ImageLeftHandle":
|
||||
scaleX = (currentBounds.Width - deltaX) / currentBounds.Width;
|
||||
translateX = deltaX;
|
||||
break;
|
||||
case "ImageRightHandle":
|
||||
scaleX = (currentBounds.Width + deltaX) / currentBounds.Width;
|
||||
break;
|
||||
var tt = tg2.Children.OfType<TranslateTransform>().FirstOrDefault();
|
||||
if (tt != null) { tx = tt.X; ty = tt.Y; }
|
||||
}
|
||||
|
||||
// 限制缩放范围
|
||||
scaleX = Math.Max(0.1, Math.Min(scaleX, 5.0));
|
||||
scaleY = Math.Max(0.1, Math.Min(scaleY, 5.0));
|
||||
|
||||
// 应用缩放
|
||||
scaleTransform.ScaleX *= scaleX;
|
||||
scaleTransform.ScaleY *= scaleY;
|
||||
|
||||
// 应用平移
|
||||
translateTransform.X += translateX;
|
||||
translateTransform.Y += translateY;
|
||||
|
||||
// 更新选择点位置
|
||||
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
|
||||
|
||||
if (BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
UpdateImageSelectionToolbarPosition(element);
|
||||
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);
|
||||
LogHelper.WriteLogToFile($"更新图片选中框位置失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private double GetElementRotationAngle(FrameworkElement element)
|
||||
{
|
||||
if (element?.RenderTransform is TransformGroup tg)
|
||||
{
|
||||
var rt = tg.Children.OfType<RotateTransform>().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<TranslateTransform>().FirstOrDefault();
|
||||
var rt = tg.Children.OfType<RotateTransform>().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<ScaleTransform>().FirstOrDefault();
|
||||
var translateTransform = transformGroup.Children.OfType<TranslateTransform>().FirstOrDefault();
|
||||
var rotateTransform = transformGroup.Children.OfType<RotateTransform>().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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2275,7 +2275,7 @@ namespace Ink_Canvas
|
||||
}
|
||||
else
|
||||
{
|
||||
var slides = await Task.Run(BuildPptPreviewItems);
|
||||
var slides = await RunOnStaAsync(BuildPptPreviewItems);
|
||||
if (slides == null || slides.Count == 0)
|
||||
{
|
||||
LogHelper.WriteLogToFile("PPT增强预览未生成可用缩略图,改用默认导航", LogHelper.LogType.Warning);
|
||||
@@ -2360,8 +2360,14 @@ namespace Ink_Canvas
|
||||
}
|
||||
|
||||
/// <summary>在 MainWindow 加载完成后调用,把 4 个 PptNavBar 的事件接到本类。</summary>
|
||||
private bool _pptNavBarsWired;
|
||||
|
||||
private void WirePptNavBars()
|
||||
{
|
||||
// InitializePPTManagers 可能被多次调用(切换 COM/ROT、设置变更等)。
|
||||
// PptNavBar 事件若在同一控件上重复订阅,会导致翻页、长按、预览展开等逻辑成倍触发。
|
||||
if (_pptNavBarsWired) return;
|
||||
|
||||
var bars = new[]
|
||||
{
|
||||
LeftBottomPanelForPPTNavigation,
|
||||
@@ -2390,6 +2396,8 @@ namespace Ink_Canvas
|
||||
bar.SlideSelected += (s, slideNumber) => OnPptNavBarSlideSelected(captured, slideNumber);
|
||||
bar.PreviewExpandedChanged += (s, expanded) => OnPptNavBarPreviewExpandedChanged(captured, expanded);
|
||||
}
|
||||
|
||||
_pptNavBarsWired = true;
|
||||
}
|
||||
|
||||
private bool _suppressPreviewExpandedSync;
|
||||
@@ -2460,6 +2468,22 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<T> RunOnStaAsync<T>(Func<T> func)
|
||||
{
|
||||
// Office interop 要求 STA + COM 单元;Task.Run 跑到 MTA 线程池里会触发 RPC_E_WRONG_THREAD
|
||||
// 等随机 COM 失败,表现为增强预览空白或崩溃。显式创建 STA worker 在其中执行导出。
|
||||
var tcs = new TaskCompletionSource<T>();
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
try { tcs.SetResult(func()); }
|
||||
catch (Exception ex) { tcs.SetException(ex); }
|
||||
});
|
||||
thread.IsBackground = true;
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private List<PptEnhancedPreviewItem> BuildPptPreviewItems()
|
||||
{
|
||||
var result = new List<PptEnhancedPreviewItem>();
|
||||
|
||||
@@ -1291,11 +1291,13 @@ namespace Ink_Canvas
|
||||
// 清除之前的更新状态
|
||||
AvailableLatestVersion = null;
|
||||
AvailableLatestLineGroup = null;
|
||||
AvailableLatestReleaseNotes = null;
|
||||
|
||||
// 使用当前选择的更新通道检查更新
|
||||
var (remoteVersion, lineGroup, apiReleaseNotes) = await AutoUpdateHelper.CheckForUpdates(Settings.Startup.UpdateChannel);
|
||||
AvailableLatestVersion = remoteVersion;
|
||||
AvailableLatestLineGroup = lineGroup;
|
||||
AvailableLatestReleaseNotes = apiReleaseNotes;
|
||||
|
||||
if (AvailableLatestVersion != null)
|
||||
{
|
||||
|
||||
@@ -2175,6 +2175,9 @@ Hide</value>
|
||||
<data name="FloatingBar_GestureButton" xml:space="preserve">
|
||||
<value>Gesture</value>
|
||||
</data>
|
||||
<data name="FloatingBar_ExitButton" xml:space="preserve">
|
||||
<value>Exit</value>
|
||||
</data>
|
||||
<data name="FloatingBar_GesturePanelTitle" xml:space="preserve">
|
||||
<value>Gesture options</value>
|
||||
</data>
|
||||
@@ -2734,7 +2737,22 @@ Hide</value>
|
||||
<value>Logs</value>
|
||||
</data>
|
||||
<data name="Storage_Logs_Desc" xml:space="preserve">
|
||||
<value>.txt logs and crash reports under Logs / Crashs. Cleanable.</value>
|
||||
<value>.txt logs and crash reports under Logs / Crashes. Cleanable.</value>
|
||||
</data>
|
||||
<data name="Hotkey_NotSet" xml:space="preserve">
|
||||
<value>Not set</value>
|
||||
</data>
|
||||
<data name="Settings_Nav_Security" xml:space="preserve">
|
||||
<value>Security</value>
|
||||
</data>
|
||||
<data name="Settings_Nav_Security_Tooltip" xml:space="preserve">
|
||||
<value>Security password and process protection</value>
|
||||
</data>
|
||||
<data name="Settings_Nav_Hotkey" xml:space="preserve">
|
||||
<value>Hotkeys</value>
|
||||
</data>
|
||||
<data name="Settings_Nav_Hotkey_Tooltip" xml:space="preserve">
|
||||
<value>Hotkey settings</value>
|
||||
</data>
|
||||
<data name="Storage_Ink_Header" xml:space="preserve">
|
||||
<value>Ink</value>
|
||||
@@ -2796,4 +2814,46 @@ Hide</value>
|
||||
<data name="Storage_NavTooltip" xml:space="preserve">
|
||||
<value>View and clean storage used by ICC CE</value>
|
||||
</data>
|
||||
<data name="Debug_ShowConsole_Header" xml:space="preserve">
|
||||
<value>Show debug console</value>
|
||||
</data>
|
||||
<data name="Debug_ShowConsole_Desc" xml:space="preserve">
|
||||
<value>Show a separate console window for live log output (takes effect immediately; if "Enable logging" is off in settings, no content will be emitted).</value>
|
||||
</data>
|
||||
<data name="Automation_UseCustomSaveFileName_Header" xml:space="preserve">
|
||||
<value>Use custom save file name</value>
|
||||
</data>
|
||||
<data name="Automation_UseCustomSaveFileName_Desc" xml:space="preserve">
|
||||
<value>When enabled, choose how saved files are named</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileNameFormat" xml:space="preserve">
|
||||
<value>File name format</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_Timestamp" xml:space="preserve">
|
||||
<value>Timestamp (default)</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_Date" xml:space="preserve">
|
||||
<value>Date</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_DateTime" xml:space="preserve">
|
||||
<value>Date + Time</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_DateMode" xml:space="preserve">
|
||||
<value>Date + Mode</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_DateModePage" xml:space="preserve">
|
||||
<value>Date + Mode + Page</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_DateModePageCount" xml:space="preserve">
|
||||
<value>Date + Mode + Page + Stroke count</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_Custom" xml:space="preserve">
|
||||
<value>Custom...</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_CustomTemplate_Header" xml:space="preserve">
|
||||
<value>Custom template</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_CustomTemplate_Desc" xml:space="preserve">
|
||||
<value>Available placeholders: {date} {time} {datetime} {mode} {page} {count} {type}</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -2218,6 +2218,9 @@
|
||||
<data name="FloatingBar_GestureButton" xml:space="preserve">
|
||||
<value>手势</value>
|
||||
</data>
|
||||
<data name="FloatingBar_ExitButton" xml:space="preserve">
|
||||
<value>退出</value>
|
||||
</data>
|
||||
<data name="FloatingBar_GesturePanelTitle" xml:space="preserve">
|
||||
<value>手势选项</value>
|
||||
</data>
|
||||
@@ -2779,6 +2782,21 @@
|
||||
<data name="Storage_Logs_Desc" xml:space="preserve">
|
||||
<value>位于 Logs / Crashs 目录下的 .txt 日志与崩溃报告,可清理</value>
|
||||
</data>
|
||||
<data name="Hotkey_NotSet" xml:space="preserve">
|
||||
<value>未设置</value>
|
||||
</data>
|
||||
<data name="Settings_Nav_Security" xml:space="preserve">
|
||||
<value>安全</value>
|
||||
</data>
|
||||
<data name="Settings_Nav_Security_Tooltip" xml:space="preserve">
|
||||
<value>安全密码与进程保护</value>
|
||||
</data>
|
||||
<data name="Settings_Nav_Hotkey" xml:space="preserve">
|
||||
<value>快捷键</value>
|
||||
</data>
|
||||
<data name="Settings_Nav_Hotkey_Tooltip" xml:space="preserve">
|
||||
<value>快捷键设置</value>
|
||||
</data>
|
||||
<data name="Storage_Ink_Header" xml:space="preserve">
|
||||
<value>墨迹</value>
|
||||
</data>
|
||||
@@ -2839,4 +2857,46 @@
|
||||
<data name="Storage_NavTooltip" xml:space="preserve">
|
||||
<value>查看与清理 ICC CE 占用的存储空间</value>
|
||||
</data>
|
||||
<data name="Debug_ShowConsole_Header" xml:space="preserve">
|
||||
<value>显示调试窗口</value>
|
||||
</data>
|
||||
<data name="Debug_ShowConsole_Desc" xml:space="preserve">
|
||||
<value>显示一个独立的控制台窗口,用于实时输出日志(开启后立即生效;关闭设置中的“启用日志记录”将不会输出内容)。</value>
|
||||
</data>
|
||||
<data name="Automation_UseCustomSaveFileName_Header" xml:space="preserve">
|
||||
<value>使用自定义保存文件名</value>
|
||||
</data>
|
||||
<data name="Automation_UseCustomSaveFileName_Desc" xml:space="preserve">
|
||||
<value>开启后可选择保存文件的命名方式</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileNameFormat" xml:space="preserve">
|
||||
<value>文件名格式</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_Timestamp" xml:space="preserve">
|
||||
<value>时间戳(默认)</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_Date" xml:space="preserve">
|
||||
<value>日期</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_DateTime" xml:space="preserve">
|
||||
<value>日期 + 时间</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_DateMode" xml:space="preserve">
|
||||
<value>日期 + 模式</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_DateModePage" xml:space="preserve">
|
||||
<value>日期 + 模式 + 页码</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_DateModePageCount" xml:space="preserve">
|
||||
<value>日期 + 模式 + 页码 + 笔画数</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_Custom" xml:space="preserve">
|
||||
<value>自定义...</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_CustomTemplate_Header" xml:space="preserve">
|
||||
<value>自定义模板</value>
|
||||
</data>
|
||||
<data name="Automation_SaveFileName_CustomTemplate_Desc" xml:space="preserve">
|
||||
<value>可用占位符:{date} {time} {datetime} {mode} {page} {count} {type}</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
|
||||
xmlns:i18n="clr-namespace:Ink_Canvas.MarkupExtensions"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="68"
|
||||
d:DesignWidth="600">
|
||||
@@ -20,7 +21,7 @@
|
||||
FontFamily="Consolas"
|
||||
FontWeight="SemiBold"
|
||||
Click="BtnSetHotkey_Click">
|
||||
<TextBlock x:Name="CurrentHotkeyTextBlock" Text="未设置" />
|
||||
<TextBlock x:Name="CurrentHotkeyTextBlock" Text="{i18n:I18n Key=Hotkey_NotSet}" />
|
||||
</Button>
|
||||
</ui:SettingsCard>
|
||||
</UserControl>
|
||||
@@ -1,3 +1,4 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
@@ -68,7 +69,7 @@ namespace Ink_Canvas.Windows
|
||||
{
|
||||
if (_currentKey == Key.None)
|
||||
{
|
||||
CurrentHotkeyTextBlock.Text = "未设置";
|
||||
CurrentHotkeyTextBlock.Text = LocalizationHelper.GetString("Hotkey_NotSet");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -0,0 +1,513 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Ink_Canvas.Windows
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用 Win32 Magnification API (Magnification.dll)。
|
||||
/// </summary>
|
||||
internal class MagnifierWindow : Window
|
||||
{
|
||||
#region P/Invoke
|
||||
|
||||
private const string MagnifierClassName = "Magnifier";
|
||||
private const int WS_CHILD = 0x40000000;
|
||||
private const int WS_VISIBLE = 0x10000000;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct RECT { public int left, top, right, bottom; public RECT(int l, int t, int r, int b) { left = l; top = t; right = r; bottom = b; } }
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MAGTRANSFORM
|
||||
{
|
||||
public float m00, m01, m02;
|
||||
public float m10, m11, m12;
|
||||
public float m20, m21, m22;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern IntPtr CreateWindowEx(int dwExStyle, string lpClassName, string lpWindowName, int dwStyle,
|
||||
int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
|
||||
|
||||
[DllImport("user32.dll")] private static extern bool DestroyWindow(IntPtr hWnd);
|
||||
[DllImport("user32.dll")] private static extern bool MoveWindow(IntPtr hWnd, int x, int y, int w, int h, bool repaint);
|
||||
[DllImport("user32.dll")] private static extern bool InvalidateRect(IntPtr hWnd, IntPtr lpRect, bool bErase);
|
||||
|
||||
[DllImport("Magnification.dll")] private static extern bool MagInitialize();
|
||||
[DllImport("Magnification.dll")] private static extern bool MagUninitialize();
|
||||
[DllImport("Magnification.dll")] private static extern bool MagSetWindowSource(IntPtr hwnd, RECT rect);
|
||||
[DllImport("Magnification.dll")] private static extern bool MagSetWindowTransform(IntPtr hwnd, ref MAGTRANSFORM pTransform);
|
||||
[DllImport("Magnification.dll")] private static extern bool MagSetWindowFilterList(IntPtr hwnd, int dwFilterMode, int count, IntPtr[] pHWND);
|
||||
private const int MW_FILTERMODE_EXCLUDE = 0;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 单例
|
||||
|
||||
private static MagnifierWindow _instance;
|
||||
public static bool HasInstance => _instance != null;
|
||||
public static event EventHandler Closed2;
|
||||
|
||||
public static void Show(float zoom)
|
||||
{
|
||||
if (_instance != null) { _instance.Activate(); _instance.Zoom = zoom; return; }
|
||||
_instance = new MagnifierWindow { Zoom = zoom };
|
||||
_instance.Closed += (s, e) =>
|
||||
{
|
||||
_instance = null;
|
||||
Closed2?.Invoke(null, EventArgs.Empty);
|
||||
};
|
||||
_instance.Show();
|
||||
}
|
||||
|
||||
public static void HideInstance() => _instance?.Close();
|
||||
public static void SetZoom(float zoom) { if (_instance != null) _instance.Zoom = zoom; }
|
||||
|
||||
#endregion
|
||||
|
||||
// 选择框(放大区域)在 Canvas 中的几何属性 (DIP)
|
||||
private double _boxLeft = 200;
|
||||
private double _boxTop = 150;
|
||||
private double _boxWidth = 520;
|
||||
private double _boxHeight = 360;
|
||||
private const double MinBoxW = 200;
|
||||
private const double MinBoxH = 140;
|
||||
|
||||
private System.Windows.Controls.Canvas _canvas;
|
||||
private Rectangle _overlay; // 半透明遮罩
|
||||
private Border _selectionBorder; // 白色边框选择框
|
||||
private MagnifierHost _magHost;
|
||||
private System.Windows.Controls.Border _toolbar;
|
||||
private Ellipse[] _handles; // 8 个控点
|
||||
private ToggleButton _blackoutButton;
|
||||
private bool _blackoutOn;
|
||||
|
||||
private IntPtr _magHwnd;
|
||||
private HwndSource _hwndSource;
|
||||
private DispatcherTimer _timer;
|
||||
private bool _magInitialized;
|
||||
|
||||
private float _zoom = 2.0f;
|
||||
public float Zoom
|
||||
{
|
||||
get => _zoom;
|
||||
set
|
||||
{
|
||||
_zoom = Math.Max(1.1f, Math.Min(8.0f, value));
|
||||
if (_magHwnd != IntPtr.Zero) ApplyTransform();
|
||||
}
|
||||
}
|
||||
|
||||
private MagnifierWindow()
|
||||
{
|
||||
Title = "聚焦放大镜";
|
||||
WindowStyle = WindowStyle.None;
|
||||
ResizeMode = ResizeMode.NoResize;
|
||||
AllowsTransparency = true;
|
||||
Background = Brushes.Transparent;
|
||||
Topmost = true;
|
||||
ShowInTaskbar = false;
|
||||
WindowState = WindowState.Maximized;
|
||||
|
||||
_canvas = new System.Windows.Controls.Canvas { Background = Brushes.Transparent };
|
||||
|
||||
// 半透明遮罩:固定浅色,挖空选择框
|
||||
_overlay = new Rectangle
|
||||
{
|
||||
Fill = new SolidColorBrush(Color.FromArgb(102, 0, 0, 0)),
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
_canvas.Children.Add(_overlay);
|
||||
|
||||
// 选择框
|
||||
_selectionBorder = new Border
|
||||
{
|
||||
BorderBrush = Brushes.White,
|
||||
BorderThickness = new Thickness(1.5),
|
||||
Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)), // 几乎透明,可命中
|
||||
Cursor = Cursors.SizeAll,
|
||||
SnapsToDevicePixels = true
|
||||
};
|
||||
_selectionBorder.MouseLeftButtonDown += SelectionBorder_MouseDown;
|
||||
_selectionBorder.MouseMove += SelectionBorder_MouseMove;
|
||||
_selectionBorder.MouseLeftButtonUp += SelectionBorder_MouseUp;
|
||||
_magHost = new MagnifierHost(this);
|
||||
_selectionBorder.Child = _magHost;
|
||||
_canvas.Children.Add(_selectionBorder);
|
||||
|
||||
// 8 个控点
|
||||
_handles = new Ellipse[8];
|
||||
string[] tags = { "NW", "N", "NE", "E", "SE", "S", "SW", "W" };
|
||||
Cursor[] cursors = { Cursors.SizeNWSE, Cursors.SizeNS, Cursors.SizeNESW, Cursors.SizeWE,
|
||||
Cursors.SizeNWSE, Cursors.SizeNS, Cursors.SizeNESW, Cursors.SizeWE };
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
var h = new Ellipse
|
||||
{
|
||||
Width = 12,
|
||||
Height = 12,
|
||||
Fill = Brushes.White,
|
||||
Stroke = new SolidColorBrush(Color.FromArgb(220, 0, 0, 0)),
|
||||
StrokeThickness = 1,
|
||||
Cursor = cursors[i],
|
||||
Tag = tags[i]
|
||||
};
|
||||
h.MouseLeftButtonDown += Handle_MouseDown;
|
||||
h.MouseMove += Handle_MouseMove;
|
||||
h.MouseLeftButtonUp += Handle_MouseUp;
|
||||
_handles[i] = h;
|
||||
_canvas.Children.Add(h);
|
||||
}
|
||||
|
||||
_toolbar = BuildToolbar();
|
||||
_canvas.Children.Add(_toolbar);
|
||||
|
||||
Content = _canvas;
|
||||
|
||||
Loaded += (s, e) =>
|
||||
{
|
||||
LayoutAll();
|
||||
};
|
||||
SizeChanged += (s, e) => LayoutAll();
|
||||
KeyDown += (s, e) => { if (e.Key == Key.Escape) Close(); };
|
||||
}
|
||||
|
||||
private System.Windows.Controls.Border BuildToolbar()
|
||||
{
|
||||
var bar = new System.Windows.Controls.Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(245, 26, 26, 26)),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(8, 4, 8, 4),
|
||||
SnapsToDevicePixels = true
|
||||
};
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
|
||||
_blackoutButton = new ToggleButton { Content = MakeBtnContent("💡", "关灯") };
|
||||
StyleToolButton(_blackoutButton);
|
||||
_blackoutButton.Checked += (s, e) => SetBlackout(true);
|
||||
_blackoutButton.Unchecked += (s, e) => SetBlackout(false);
|
||||
|
||||
var closeBtn = new System.Windows.Controls.Button { Content = MakeBtnContent("✕", "关闭") };
|
||||
StyleToolButton(closeBtn);
|
||||
closeBtn.Click += (s, e) => Close();
|
||||
|
||||
sp.Children.Add(_blackoutButton);
|
||||
sp.Children.Add(closeBtn);
|
||||
bar.Child = sp;
|
||||
return bar;
|
||||
}
|
||||
|
||||
private static FrameworkElement MakeBtnContent(string icon, string text)
|
||||
{
|
||||
var sp = new StackPanel { Orientation = Orientation.Vertical, HorizontalAlignment = HorizontalAlignment.Center };
|
||||
sp.Children.Add(new TextBlock { Text = icon, FontSize = 16, Foreground = Brushes.White, HorizontalAlignment = HorizontalAlignment.Center });
|
||||
sp.Children.Add(new TextBlock { Text = text, FontSize = 11, Foreground = Brushes.White, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 1, 0, 0) });
|
||||
return sp;
|
||||
}
|
||||
|
||||
private static void StyleToolButton(ButtonBase b)
|
||||
{
|
||||
b.Width = 56;
|
||||
b.Height = 46;
|
||||
b.Margin = new Thickness(3, 0, 3, 0);
|
||||
b.Background = Brushes.Transparent;
|
||||
b.BorderThickness = new Thickness(0);
|
||||
b.Foreground = Brushes.White;
|
||||
b.Cursor = Cursors.Hand;
|
||||
b.Padding = new Thickness(0);
|
||||
b.Template = BuildToolButtonTemplate();
|
||||
}
|
||||
|
||||
private static ControlTemplate BuildToolButtonTemplate()
|
||||
{
|
||||
var t = new ControlTemplate(typeof(ButtonBase));
|
||||
var border = new FrameworkElementFactory(typeof(System.Windows.Controls.Border));
|
||||
border.Name = "Bd";
|
||||
border.SetValue(System.Windows.Controls.Border.BackgroundProperty, new TemplateBindingExtension(System.Windows.Controls.Control.BackgroundProperty));
|
||||
border.SetValue(System.Windows.Controls.Border.CornerRadiusProperty, new CornerRadius(6));
|
||||
var cp = new FrameworkElementFactory(typeof(ContentPresenter));
|
||||
cp.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Center);
|
||||
cp.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
|
||||
border.AppendChild(cp);
|
||||
t.VisualTree = border;
|
||||
|
||||
var hover = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true };
|
||||
hover.Setters.Add(new Setter(System.Windows.Controls.Border.BackgroundProperty, new SolidColorBrush(Color.FromArgb(60, 255, 255, 255)), "Bd"));
|
||||
t.Triggers.Add(hover);
|
||||
|
||||
var checkedT = new Trigger { Property = ToggleButton.IsCheckedProperty, Value = true };
|
||||
checkedT.Setters.Add(new Setter(System.Windows.Controls.Border.BackgroundProperty, new SolidColorBrush(Color.FromRgb(0x05, 0x96, 0x69)), "Bd"));
|
||||
t.Triggers.Add(checkedT);
|
||||
return t;
|
||||
}
|
||||
|
||||
#region 布局
|
||||
|
||||
private void LayoutAll()
|
||||
{
|
||||
double w = _canvas.ActualWidth;
|
||||
double h = _canvas.ActualHeight;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
// 首次居中
|
||||
if (_boxLeft + _boxWidth > w) _boxLeft = Math.Max(0, w - _boxWidth);
|
||||
if (_boxTop + _boxHeight > h) _boxTop = Math.Max(0, h - _boxHeight);
|
||||
|
||||
// 遮罩占满
|
||||
_overlay.Width = w;
|
||||
_overlay.Height = h;
|
||||
System.Windows.Controls.Canvas.SetLeft(_overlay, 0);
|
||||
System.Windows.Controls.Canvas.SetTop(_overlay, 0);
|
||||
UpdateOverlayClip();
|
||||
|
||||
// 选择框
|
||||
_selectionBorder.Width = _boxWidth;
|
||||
_selectionBorder.Height = _boxHeight;
|
||||
System.Windows.Controls.Canvas.SetLeft(_selectionBorder, _boxLeft);
|
||||
System.Windows.Controls.Canvas.SetTop(_selectionBorder, _boxTop);
|
||||
|
||||
// 8 个控点(中心定位)
|
||||
PositionHandle(0, _boxLeft, _boxTop); // NW
|
||||
PositionHandle(1, _boxLeft + _boxWidth / 2, _boxTop); // N
|
||||
PositionHandle(2, _boxLeft + _boxWidth, _boxTop); // NE
|
||||
PositionHandle(3, _boxLeft + _boxWidth, _boxTop + _boxHeight / 2); // E
|
||||
PositionHandle(4, _boxLeft + _boxWidth, _boxTop + _boxHeight); // SE
|
||||
PositionHandle(5, _boxLeft + _boxWidth / 2, _boxTop + _boxHeight); // S
|
||||
PositionHandle(6, _boxLeft, _boxTop + _boxHeight); // SW
|
||||
PositionHandle(7, _boxLeft, _boxTop + _boxHeight / 2); // W
|
||||
|
||||
// 工具栏:选择框正下方居中;若超出则放上方
|
||||
_toolbar.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
double tw = _toolbar.DesiredSize.Width, th = _toolbar.DesiredSize.Height;
|
||||
double tx = _boxLeft + _boxWidth / 2 - tw / 2;
|
||||
double ty = _boxTop + _boxHeight + 12;
|
||||
if (ty + th > h) ty = _boxTop - th - 12;
|
||||
tx = Math.Max(8, Math.Min(w - tw - 8, tx));
|
||||
System.Windows.Controls.Canvas.SetLeft(_toolbar, tx);
|
||||
System.Windows.Controls.Canvas.SetTop(_toolbar, ty);
|
||||
}
|
||||
|
||||
private void PositionHandle(int i, double cx, double cy)
|
||||
{
|
||||
var dot = _handles[i];
|
||||
System.Windows.Controls.Canvas.SetLeft(dot, cx - dot.Width / 2);
|
||||
System.Windows.Controls.Canvas.SetTop(dot, cy - dot.Height / 2);
|
||||
}
|
||||
|
||||
private void UpdateOverlayClip()
|
||||
{
|
||||
// 用 EvenOdd Geometry 在 overlay 上挖空选择框区域
|
||||
double w = _canvas.ActualWidth;
|
||||
double h = _canvas.ActualHeight;
|
||||
if (w <= 0 || h <= 0) { _overlay.Clip = null; return; }
|
||||
|
||||
var outer = new RectangleGeometry(new Rect(0, 0, w, h));
|
||||
var inner = new RectangleGeometry(new Rect(_boxLeft, _boxTop, _boxWidth, _boxHeight));
|
||||
var combined = new CombinedGeometry(GeometryCombineMode.Exclude, outer, inner);
|
||||
_overlay.Clip = combined;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 拖动
|
||||
|
||||
private bool _draggingBox;
|
||||
private Point _dragStart;
|
||||
private double _dragBoxLeft, _dragBoxTop;
|
||||
|
||||
private void SelectionBorder_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_draggingBox = true;
|
||||
_dragStart = e.GetPosition(_canvas);
|
||||
_dragBoxLeft = _boxLeft;
|
||||
_dragBoxTop = _boxTop;
|
||||
_selectionBorder.CaptureMouse();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void SelectionBorder_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!_draggingBox) return;
|
||||
var p = e.GetPosition(_canvas);
|
||||
_boxLeft = Math.Max(0, Math.Min(_canvas.ActualWidth - _boxWidth, _dragBoxLeft + p.X - _dragStart.X));
|
||||
_boxTop = Math.Max(0, Math.Min(_canvas.ActualHeight - _boxHeight, _dragBoxTop + p.Y - _dragStart.Y));
|
||||
LayoutAll();
|
||||
}
|
||||
|
||||
private void SelectionBorder_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!_draggingBox) return;
|
||||
_draggingBox = false;
|
||||
_selectionBorder.ReleaseMouseCapture();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 缩放
|
||||
|
||||
private bool _resizing;
|
||||
private string _resizeTag;
|
||||
private Point _resizeStart;
|
||||
private double _rL, _rT, _rW, _rH;
|
||||
|
||||
private void Handle_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var el = (FrameworkElement)sender;
|
||||
_resizing = true;
|
||||
_resizeTag = (string)el.Tag;
|
||||
_resizeStart = e.GetPosition(_canvas);
|
||||
_rL = _boxLeft; _rT = _boxTop; _rW = _boxWidth; _rH = _boxHeight;
|
||||
el.CaptureMouse();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void Handle_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!_resizing) return;
|
||||
var p = e.GetPosition(_canvas);
|
||||
double dx = p.X - _resizeStart.X;
|
||||
double dy = p.Y - _resizeStart.Y;
|
||||
|
||||
double nL = _rL, nT = _rT, nW = _rW, nH = _rH;
|
||||
switch (_resizeTag)
|
||||
{
|
||||
case "NW": nL = _rL + dx; nT = _rT + dy; nW = _rW - dx; nH = _rH - dy; break;
|
||||
case "N": nT = _rT + dy; nH = _rH - dy; break;
|
||||
case "NE": nT = _rT + dy; nW = _rW + dx; nH = _rH - dy; break;
|
||||
case "E": nW = _rW + dx; break;
|
||||
case "SE": nW = _rW + dx; nH = _rH + dy; break;
|
||||
case "S": nH = _rH + dy; break;
|
||||
case "SW": nL = _rL + dx; nW = _rW - dx; nH = _rH + dy; break;
|
||||
case "W": nL = _rL + dx; nW = _rW - dx; break;
|
||||
}
|
||||
if (nW < MinBoxW) { if (_resizeTag.Contains("W")) nL -= MinBoxW - nW; nW = MinBoxW; }
|
||||
if (nH < MinBoxH) { if (_resizeTag.Contains("N")) nT -= MinBoxH - nH; nH = MinBoxH; }
|
||||
|
||||
// 限制在窗口内
|
||||
nL = Math.Max(0, nL);
|
||||
nT = Math.Max(0, nT);
|
||||
if (nL + nW > _canvas.ActualWidth) nW = _canvas.ActualWidth - nL;
|
||||
if (nT + nH > _canvas.ActualHeight) nH = _canvas.ActualHeight - nT;
|
||||
|
||||
_boxLeft = nL; _boxTop = nT; _boxWidth = nW; _boxHeight = nH;
|
||||
LayoutAll();
|
||||
}
|
||||
|
||||
private void Handle_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!_resizing) return;
|
||||
_resizing = false;
|
||||
((FrameworkElement)sender).ReleaseMouseCapture();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 关灯
|
||||
|
||||
private void SetBlackout(bool on)
|
||||
{
|
||||
_blackoutOn = on;
|
||||
_overlay.Fill = new SolidColorBrush(on
|
||||
? Color.FromArgb(245, 0, 0, 0)
|
||||
: Color.FromArgb(102, 0, 0, 0));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Magnifier 子窗口
|
||||
|
||||
private class MagnifierHost : HwndHost
|
||||
{
|
||||
private readonly MagnifierWindow _owner;
|
||||
public IntPtr MagHwnd { get; private set; }
|
||||
public MagnifierHost(MagnifierWindow owner) { _owner = owner; }
|
||||
|
||||
protected override HandleRef BuildWindowCore(HandleRef hwndParent)
|
||||
{
|
||||
if (!MagInitialize()) throw new InvalidOperationException("MagInitialize 失败");
|
||||
_owner._magInitialized = true;
|
||||
|
||||
MagHwnd = CreateWindowEx(
|
||||
0, MagnifierClassName, "ICCMagnifier",
|
||||
WS_CHILD | WS_VISIBLE,
|
||||
0, 0, 100, 100,
|
||||
hwndParent.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
|
||||
_owner._magHwnd = MagHwnd;
|
||||
return new HandleRef(this, MagHwnd);
|
||||
}
|
||||
|
||||
protected override void DestroyWindowCore(HandleRef hwnd)
|
||||
{
|
||||
if (hwnd.Handle != IntPtr.Zero) DestroyWindow(hwnd.Handle);
|
||||
MagHwnd = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnSourceInitialized(EventArgs e)
|
||||
{
|
||||
base.OnSourceInitialized(e);
|
||||
_hwndSource = (HwndSource)PresentationSource.FromVisual(this);
|
||||
ApplyTransform();
|
||||
if (_hwndSource != null && _magHwnd != IntPtr.Zero)
|
||||
MagSetWindowFilterList(_magHwnd, MW_FILTERMODE_EXCLUDE, 1, new[] { _hwndSource.Handle });
|
||||
|
||||
_timer = new DispatcherTimer(DispatcherPriority.Render) { Interval = TimeSpan.FromMilliseconds(33) };
|
||||
_timer.Tick += OnTick;
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
private void ApplyTransform()
|
||||
{
|
||||
if (_magHwnd == IntPtr.Zero) return;
|
||||
var m = new MAGTRANSFORM
|
||||
{
|
||||
m00 = _zoom, m01 = 0, m02 = 0,
|
||||
m10 = 0, m11 = _zoom, m12 = 0,
|
||||
m20 = 0, m21 = 0, m22 = 1.0f
|
||||
};
|
||||
MagSetWindowTransform(_magHwnd, ref m);
|
||||
}
|
||||
|
||||
private void OnTick(object sender, EventArgs e)
|
||||
{
|
||||
if (_magHwnd == IntPtr.Zero || _magHost == null) return;
|
||||
if (_magHost.RenderSize.Width <= 0) return;
|
||||
|
||||
// 选择框在屏幕物理像素坐标
|
||||
var hostOffset = _magHost.TransformToAncestor(this).Transform(new Point(0, 0));
|
||||
var tl = PointToScreen(hostOffset);
|
||||
var br = PointToScreen(new Point(hostOffset.X + _magHost.RenderSize.Width,
|
||||
hostOffset.Y + _magHost.RenderSize.Height));
|
||||
int viewW = (int)(br.X - tl.X);
|
||||
int viewH = (int)(br.Y - tl.Y);
|
||||
if (viewW <= 0 || viewH <= 0) return;
|
||||
|
||||
int srcW = Math.Max(1, (int)(viewW / _zoom));
|
||||
int srcH = Math.Max(1, (int)(viewH / _zoom));
|
||||
int cx = (int)((tl.X + br.X) / 2);
|
||||
int cy = (int)((tl.Y + br.Y) / 2);
|
||||
var src = new RECT(cx - srcW / 2, cy - srcH / 2,
|
||||
cx - srcW / 2 + srcW, cy - srcH / 2 + srcH);
|
||||
MagSetWindowSource(_magHwnd, src);
|
||||
InvalidateRect(_magHwnd, IntPtr.Zero, false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
if (_timer != null) { _timer.Stop(); _timer.Tick -= OnTick; _timer = null; }
|
||||
if (_magInitialized) { MagUninitialize(); _magInitialized = false; }
|
||||
base.OnClosed(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,69 +3,127 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="400" d:DesignWidth="300">
|
||||
d:DesignHeight="420" d:DesignWidth="260">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<!-- 展开动画 -->
|
||||
<!-- 展开 / 折叠动画 -->
|
||||
<Storyboard x:Key="ExpandAnimation">
|
||||
<DoubleAnimation Storyboard.TargetName="PanelTransform"
|
||||
Storyboard.TargetProperty="X"
|
||||
Duration="0:0:0.3">
|
||||
Storyboard.TargetProperty="X"
|
||||
Duration="0:0:0.28">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseOut"/>
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
|
||||
<!-- 折叠动画 -->
|
||||
<Storyboard x:Key="CollapseAnimation">
|
||||
<DoubleAnimation Storyboard.TargetName="PanelTransform"
|
||||
Storyboard.TargetProperty="X"
|
||||
Duration="0:0:0.3">
|
||||
Storyboard.TargetProperty="X"
|
||||
Duration="0:0:0.28">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseOut"/>
|
||||
<CubicEase EasingMode="EaseIn"/>
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
|
||||
<!-- Game Bar 风格扁平按钮 -->
|
||||
<Style x:Key="GameBarFlatButton" TargetType="Button">
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="10,0"/>
|
||||
<Setter Property="Height" Value="34"/>
|
||||
<Setter Property="Background" Value="#28FFFFFF"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="6"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#40FFFFFF"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#55FFFFFF"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 区段标题 -->
|
||||
<Style x:Key="GameBarSectionTitle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="10"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Opacity" Value="0.7"/>
|
||||
<Setter Property="Margin" Value="2,0,0,4"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
|
||||
<Grid>
|
||||
<Canvas x:Name="MainCanvas" HorizontalAlignment="Right" VerticalAlignment="Top">
|
||||
<!-- 主面板容器 -->
|
||||
<Grid x:Name="MainPanel" Canvas.Right="0" Canvas.Top="50">
|
||||
<Grid.RenderTransform>
|
||||
<TranslateTransform x:Name="PanelTransform" X="0"/>
|
||||
</Grid.RenderTransform>
|
||||
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="24"/>
|
||||
<ColumnDefinition Width="20"/>
|
||||
<!-- 内容区域:宽度必须为 200 (与 _collapsedOffset 对应) -->
|
||||
<ColumnDefinition Width="200"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧圆角矩形 - 箭头按钮区域 -->
|
||||
<Border x:Name="ArrowButtonBorder"
|
||||
|
||||
<!-- 内容区域外壳:圆角 + 背景 + 阴影 (只覆盖内容列,折叠后完全滑出视区) -->
|
||||
<Border Grid.Column="1"
|
||||
CornerRadius="12">
|
||||
<Border.Background>
|
||||
<SolidColorBrush x:Name="ContentBackgroundBrush" Color="#CC1F1F1F"/>
|
||||
</Border.Background>
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="18" ShadowDepth="0" Opacity="0.35" Color="Black"/>
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
|
||||
<!-- 左侧箭头交互区(独立小胶囊,折叠后仍可见) -->
|
||||
<Border x:Name="ArrowButtonBorder"
|
||||
Grid.Column="0"
|
||||
CornerRadius="6,0,0,6"
|
||||
Width="14"
|
||||
Height="44"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="7"
|
||||
Cursor="Hand"
|
||||
MouseLeftButtonDown="ArrowButton_MouseLeftButtonDown"
|
||||
MouseEnter="ArrowButton_MouseEnter"
|
||||
MouseLeave="ArrowButton_MouseLeave">
|
||||
<Border.Background>
|
||||
<SolidColorBrush x:Name="ArrowButtonBackgroundBrush" Color="#CCFFFFFF"/>
|
||||
<SolidColorBrush x:Name="ArrowButtonBackgroundBrush" Color="#CC1F1F1F"/>
|
||||
</Border.Background>
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="6" ShadowDepth="0" Opacity="0.2" Color="Black"/>
|
||||
<DropShadowEffect BlurRadius="8" ShadowDepth="0" Opacity="0.3" Color="Black"/>
|
||||
</Border.Effect>
|
||||
|
||||
<Viewbox Width="12" Height="12" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<Path x:Name="ArrowPath"
|
||||
Data="M 0,0 L 12,6 L 0,12 Z"
|
||||
|
||||
<Viewbox Width="7" Height="7"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center">
|
||||
<Path x:Name="ArrowPath"
|
||||
Data="M 0,0 L 12,6 L 0,12 Z"
|
||||
Stretch="Uniform"
|
||||
RenderTransformOrigin="0.5,0.5">
|
||||
<Path.Fill>
|
||||
<SolidColorBrush x:Name="ArrowPathFillBrush" Color="#FF000000"/>
|
||||
<SolidColorBrush x:Name="ArrowPathFillBrush" Color="White"/>
|
||||
</Path.Fill>
|
||||
<Path.RenderTransform>
|
||||
<RotateTransform x:Name="ArrowRotateTransform" Angle="180"/>
|
||||
@@ -73,72 +131,93 @@
|
||||
</Path>
|
||||
</Viewbox>
|
||||
</Border>
|
||||
|
||||
<!-- 右侧矩形 - 面板内容区域 -->
|
||||
<Border x:Name="ContentBorder"
|
||||
|
||||
<!-- 内容区域(透明,仅承担事件与裁剪) -->
|
||||
<Border x:Name="ContentBorder"
|
||||
Grid.Column="1"
|
||||
CornerRadius="0"
|
||||
Background="Transparent"
|
||||
CornerRadius="0,12,12,0"
|
||||
MouseLeftButtonDown="ContentBorder_MouseLeftButtonDown"
|
||||
MouseMove="ContentBorder_MouseMove"
|
||||
MouseLeftButtonUp="ContentBorder_MouseLeftButtonUp"
|
||||
TouchDown="ContentBorder_TouchDown"
|
||||
TouchMove="ContentBorder_TouchMove"
|
||||
TouchUp="ContentBorder_TouchUp">
|
||||
<Border.Background>
|
||||
<SolidColorBrush x:Name="ContentBackgroundBrush" Color="#CCFFFFFF"/>
|
||||
</Border.Background>
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="8" ShadowDepth="0" Opacity="0.2" Color="Black"/>
|
||||
</Border.Effect>
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel x:Name="ContentPanel" Margin="10,8" Orientation="Vertical">
|
||||
|
||||
<!-- 系统音量控制区域 -->
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧音量按钮 -->
|
||||
<Button x:Name="VolumeMuteButton"
|
||||
Grid.Column="0"
|
||||
Width="32" Height="32"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Cursor="Hand"
|
||||
Click="VolumeMuteButton_Click"
|
||||
Margin="0,0,8,0"
|
||||
Padding="0"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center">
|
||||
<Viewbox Width="20" Height="20" Stretch="Uniform">
|
||||
<Grid>
|
||||
<Path x:Name="VolumeIconPath"
|
||||
Data="M 3,9 v 6 h 4 l 5,5 v -16 l -5,5 h -4 z"
|
||||
Stretch="Uniform">
|
||||
<Path.Fill>
|
||||
<SolidColorBrush x:Name="VolumeIconFillBrush" Color="#FF000000"/>
|
||||
</Path.Fill>
|
||||
</Path>
|
||||
<Path x:Name="VolumeIconPath2"
|
||||
Data=""
|
||||
Stretch="Uniform"
|
||||
Opacity="0.5"
|
||||
Visibility="Collapsed">
|
||||
<Path.Fill>
|
||||
<SolidColorBrush x:Name="VolumeIconFillBrush2" Color="#FF000000"/>
|
||||
</Path.Fill>
|
||||
</Path>
|
||||
</Grid>
|
||||
</Viewbox>
|
||||
</Button>
|
||||
|
||||
<!-- 中间音量滑块 -->
|
||||
<Grid Grid.Column="1" VerticalAlignment="Center" Margin="0,0,8,0">
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
Padding="0">
|
||||
<StackPanel x:Name="ContentPanel" Margin="12,12,12,12" Orientation="Vertical">
|
||||
|
||||
<!-- 区段:声音 -->
|
||||
<TextBlock Text="声音" Style="{StaticResource GameBarSectionTitle}">
|
||||
<TextBlock.Foreground>
|
||||
<SolidColorBrush x:Name="MagnifierTitleForegroundBrush" Color="White"/>
|
||||
</TextBlock.Foreground>
|
||||
</TextBlock>
|
||||
|
||||
<Border CornerRadius="8" Background="#1AFFFFFF" Padding="6" Margin="0,0,0,12">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 静音按钮 -->
|
||||
<Button x:Name="VolumeMuteButton"
|
||||
Grid.Column="0"
|
||||
Width="28" Height="28"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Cursor="Hand"
|
||||
Padding="0"
|
||||
Click="VolumeMuteButton_Click"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="6">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#33FFFFFF"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Viewbox Width="16" Height="16" Stretch="Uniform">
|
||||
<Grid>
|
||||
<Path x:Name="VolumeIconPath"
|
||||
Data="M 3,9 v 6 h 4 l 5,5 v -16 l -5,5 h -4 z"
|
||||
Stretch="Uniform">
|
||||
<Path.Fill>
|
||||
<SolidColorBrush x:Name="VolumeIconFillBrush" Color="White"/>
|
||||
</Path.Fill>
|
||||
</Path>
|
||||
<Path x:Name="VolumeIconPath2"
|
||||
Data=""
|
||||
Stretch="Uniform"
|
||||
Opacity="0.5"
|
||||
Visibility="Collapsed">
|
||||
<Path.Fill>
|
||||
<SolidColorBrush x:Name="VolumeIconFillBrush2" Color="White"/>
|
||||
</Path.Fill>
|
||||
</Path>
|
||||
</Grid>
|
||||
</Viewbox>
|
||||
</Button>
|
||||
|
||||
<!-- 音量滑块 -->
|
||||
<Slider x:Name="VolumeSlider"
|
||||
Grid.Column="1"
|
||||
Margin="8,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Stylus.IsPressAndHoldEnabled="False"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
Value="50"
|
||||
@@ -147,98 +226,114 @@
|
||||
ValueChanged="VolumeSlider_ValueChanged"
|
||||
PreviewMouseLeftButtonUp="VolumeSlider_PreviewMouseLeftButtonUp"
|
||||
ManipulationCompleted="VolumeSlider_ManipulationCompleted"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 右侧音量值 -->
|
||||
<TextBlock x:Name="VolumeValueText"
|
||||
Grid.Column="2"
|
||||
Text="50%"
|
||||
FontSize="10"
|
||||
VerticalAlignment="Center"
|
||||
MinWidth="32"
|
||||
TextAlignment="Center">
|
||||
<TextBlock.Foreground>
|
||||
<SolidColorBrush x:Name="VolumeValueForegroundBrush" Color="#80000000"/>
|
||||
</TextBlock.Foreground>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<Separator x:Name="Separator1" Margin="0,0,0,8" Height="1">
|
||||
<Separator.Background>
|
||||
<SolidColorBrush x:Name="Separator1BackgroundBrush" Color="#E0E0E0"/>
|
||||
</Separator.Background>
|
||||
</Separator>
|
||||
|
||||
<!-- 图片插入按钮区域 -->
|
||||
<StackPanel Orientation="Vertical" Margin="0,0,0,8">
|
||||
<!-- 选择文件按钮 -->
|
||||
<Button x:Name="InsertImageSelectFileButton"
|
||||
Content="选择文件"
|
||||
FontSize="11"
|
||||
Height="30"
|
||||
Background="#3b82f6"
|
||||
Foreground="White"
|
||||
BorderThickness="0"
|
||||
Cursor="Hand"
|
||||
Click="InsertImageSelectFileButton_Click"
|
||||
Margin="0,0,0,4">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="4"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
|
||||
<!-- 截图插入按钮 -->
|
||||
<Button x:Name="InsertImageScreenshotButton"
|
||||
Content="截图插入"
|
||||
FontSize="11"
|
||||
Height="30"
|
||||
Background="#3b82f6"
|
||||
Foreground="White"
|
||||
BorderThickness="0"
|
||||
Cursor="Hand"
|
||||
Click="InsertImageScreenshotButton_Click"
|
||||
Margin="0,0,0,0">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="4"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 聚焦放大镜(暂未设计)- 暂时隐藏 -->
|
||||
<Border x:Name="MagnifierSection" Margin="0,0,0,8" Visibility="Collapsed">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock x:Name="MagnifierTitleText" Text="聚焦放大镜" FontSize="11" FontWeight="Bold" Margin="0,0,0,6">
|
||||
|
||||
<!-- 数值 -->
|
||||
<TextBlock x:Name="VolumeValueText"
|
||||
Grid.Column="2"
|
||||
Text="50%"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"
|
||||
MinWidth="32"
|
||||
TextAlignment="Right">
|
||||
<TextBlock.Foreground>
|
||||
<SolidColorBrush x:Name="MagnifierTitleForegroundBrush" Color="#FF000000"/>
|
||||
<SolidColorBrush x:Name="VolumeValueForegroundBrush" Color="#E6FFFFFF"/>
|
||||
</TextBlock.Foreground>
|
||||
</TextBlock>
|
||||
<TextBlock x:Name="MagnifierDescText" Text="功能暂未设计" FontSize="9" FontStyle="Italic">
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 隐藏的占位分隔线(保留命名元素以满足 .cs 引用) -->
|
||||
<Separator x:Name="Separator1" Visibility="Collapsed" Height="0">
|
||||
<Separator.Background>
|
||||
<SolidColorBrush x:Name="Separator1BackgroundBrush" Color="Transparent"/>
|
||||
</Separator.Background>
|
||||
</Separator>
|
||||
|
||||
<!-- 区段:插入图片 -->
|
||||
<TextBlock Text="插入" Style="{StaticResource GameBarSectionTitle}"/>
|
||||
|
||||
<StackPanel Orientation="Vertical" Margin="0,0,0,4">
|
||||
<Button x:Name="InsertImageSelectFileButton"
|
||||
Content="选择文件"
|
||||
Style="{StaticResource GameBarFlatButton}"
|
||||
Margin="0,0,0,6"
|
||||
Click="InsertImageSelectFileButton_Click"/>
|
||||
|
||||
<Button x:Name="InsertImageScreenshotButton"
|
||||
Content="截图插入"
|
||||
Style="{StaticResource GameBarFlatButton}"
|
||||
Click="InsertImageScreenshotButton_Click"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 区段:聚焦放大镜 -->
|
||||
<Border x:Name="MagnifierSection">
|
||||
<StackPanel>
|
||||
<TextBlock x:Name="MagnifierTitleText"
|
||||
Text="聚焦放大镜"
|
||||
Style="{StaticResource GameBarSectionTitle}">
|
||||
<TextBlock.Foreground>
|
||||
<SolidColorBrush x:Name="MagnifierDescForegroundBrush" Color="#80000000"/>
|
||||
<SolidColorBrush x:Name="MagnifierTitleForeground2Brush" Color="White"/>
|
||||
</TextBlock.Foreground>
|
||||
</TextBlock>
|
||||
|
||||
<Button x:Name="MagnifierToggleButton"
|
||||
Content="开启放大镜"
|
||||
Style="{StaticResource GameBarFlatButton}"
|
||||
Margin="0,0,0,6"
|
||||
Click="MagnifierToggleButton_Click"/>
|
||||
|
||||
<Border CornerRadius="8" Background="#1AFFFFFF" Padding="6" Margin="0,0,0,4">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="缩放"
|
||||
FontSize="11"
|
||||
VerticalAlignment="Center"
|
||||
Margin="4,0,8,0"
|
||||
Foreground="White"/>
|
||||
<Slider x:Name="MagnifierZoomSlider"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Stylus.IsPressAndHoldEnabled="False"
|
||||
Minimum="1.5"
|
||||
Maximum="6"
|
||||
Value="2"
|
||||
TickFrequency="0.5"
|
||||
IsSnapToTickEnabled="True"
|
||||
ValueChanged="MagnifierZoomSlider_ValueChanged"/>
|
||||
<TextBlock x:Name="MagnifierZoomValueText"
|
||||
Grid.Column="2"
|
||||
Text="2.0x"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"
|
||||
MinWidth="36"
|
||||
TextAlignment="Right"
|
||||
Foreground="#E6FFFFFF"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="MagnifierDescText"
|
||||
Text="拖动选择框移动,拖动控制点缩放,按 ESC 关闭"
|
||||
FontSize="11"
|
||||
Margin="2,0,0,0"
|
||||
TextWrapping="Wrap">
|
||||
<TextBlock.Foreground>
|
||||
<SolidColorBrush x:Name="MagnifierDescForegroundBrush" Color="#80FFFFFF"/>
|
||||
</TextBlock.Foreground>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Canvas>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
|
||||
</UserControl>
|
||||
@@ -118,6 +118,19 @@ namespace Ink_Canvas.Windows
|
||||
Loaded += PPTQuickPanel_Loaded;
|
||||
Unloaded += PPTQuickPanel_Unloaded;
|
||||
IsVisibleChanged += PPTQuickPanel_IsVisibleChanged;
|
||||
|
||||
MagnifierWindow.Closed2 += OnMagnifierClosed;
|
||||
}
|
||||
|
||||
private void OnMagnifierClosed(object sender, EventArgs e)
|
||||
{
|
||||
Dispatcher.BeginInvoke(new Action(SyncMagnifierButtonState));
|
||||
}
|
||||
|
||||
private void SyncMagnifierButtonState()
|
||||
{
|
||||
if (MagnifierToggleButton == null) return;
|
||||
MagnifierToggleButton.Content = MagnifierWindow.HasInstance ? "关闭放大镜" : "开启放大镜";
|
||||
}
|
||||
|
||||
private void PPTQuickPanel_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
@@ -125,6 +138,7 @@ namespace Ink_Canvas.Windows
|
||||
if (Visibility == Visibility.Visible)
|
||||
{
|
||||
ApplyTheme();
|
||||
SyncMagnifierButtonState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -747,31 +761,34 @@ namespace Ink_Canvas.Windows
|
||||
|
||||
private void ArrowButton_MouseEnter(object sender, MouseEventArgs e)
|
||||
{
|
||||
// 根据当前主题设置悬停颜色
|
||||
bool isDark = ArrowButtonBackgroundBrush.Color.R < 128;
|
||||
if (isDark)
|
||||
{
|
||||
ArrowButtonBackgroundBrush.Color = Color.FromArgb(230, 32, 32, 32);
|
||||
}
|
||||
else
|
||||
{
|
||||
ArrowButtonBackgroundBrush.Color = Color.FromArgb(230, 255, 255, 255);
|
||||
}
|
||||
ArrowButtonBackgroundBrush.Color = Color.FromArgb(220, 55, 55, 55);
|
||||
}
|
||||
|
||||
private void ArrowButton_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
// 恢复主题颜色
|
||||
ApplyTheme();
|
||||
ArrowButtonBackgroundBrush.Color = Color.FromArgb(204, 31, 31, 31);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 拖动手势
|
||||
|
||||
private static bool IsWithinSlider(object source)
|
||||
{
|
||||
var d = source as DependencyObject;
|
||||
while (d != null)
|
||||
{
|
||||
if (d is Slider) return true;
|
||||
d = (d is System.Windows.Media.Visual || d is System.Windows.Media.Media3D.Visual3D)
|
||||
? System.Windows.Media.VisualTreeHelper.GetParent(d)
|
||||
: LogicalTreeHelper.GetParent(d);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ContentBorder_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is Slider) return; // 如果点击的是滑块,不处理拖动
|
||||
if (IsWithinSlider(e.OriginalSource)) return;
|
||||
|
||||
_isDragging = true;
|
||||
_dragStartPoint = e.GetPosition(MainCanvas);
|
||||
@@ -816,7 +833,7 @@ namespace Ink_Canvas.Windows
|
||||
|
||||
private void ContentBorder_TouchDown(object sender, TouchEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is Slider) return;
|
||||
if (IsWithinSlider(e.OriginalSource)) return;
|
||||
|
||||
_isDragging = true;
|
||||
_dragStartPoint = e.GetTouchPoint(MainCanvas).Position;
|
||||
@@ -1835,35 +1852,16 @@ namespace Ink_Canvas.Windows
|
||||
{
|
||||
try
|
||||
{
|
||||
bool isDarkTheme = settings.Appearance.Theme == 1 ||
|
||||
(settings.Appearance.Theme == 2 && !ThemeHelper.IsSystemThemeLight());
|
||||
|
||||
if (isDarkTheme)
|
||||
{
|
||||
// 深色主题:使用80%不透明度的深色背景
|
||||
ArrowButtonBackgroundBrush.Color = Color.FromArgb(204, 32, 32, 32); // #CC202020
|
||||
ContentBackgroundBrush.Color = Color.FromArgb(204, 32, 32, 32); // #CC202020
|
||||
ArrowPathFillBrush.Color = Colors.White;
|
||||
VolumeIconFillBrush.Color = Colors.White;
|
||||
VolumeIconFillBrush2.Color = Colors.White;
|
||||
VolumeValueForegroundBrush.Color = Color.FromArgb(200, 255, 255, 255);
|
||||
MagnifierTitleForegroundBrush.Color = Colors.White;
|
||||
MagnifierDescForegroundBrush.Color = Color.FromArgb(200, 255, 255, 255);
|
||||
Separator1BackgroundBrush.Color = Color.FromArgb(128, 255, 255, 255);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 浅色主题:使用80%不透明度的白色背景
|
||||
ArrowButtonBackgroundBrush.Color = Color.FromArgb(204, 255, 255, 255); // #CCFFFFFF
|
||||
ContentBackgroundBrush.Color = Color.FromArgb(204, 255, 255, 255); // #CCFFFFFF
|
||||
ArrowPathFillBrush.Color = Colors.Black;
|
||||
VolumeIconFillBrush.Color = Colors.Black;
|
||||
VolumeIconFillBrush2.Color = Colors.Black;
|
||||
VolumeValueForegroundBrush.Color = Color.FromArgb(128, 0, 0, 0);
|
||||
MagnifierTitleForegroundBrush.Color = Colors.Black;
|
||||
MagnifierDescForegroundBrush.Color = Color.FromArgb(128, 0, 0, 0);
|
||||
Separator1BackgroundBrush.Color = Color.FromArgb(255, 224, 224, 224);
|
||||
}
|
||||
// Game Bar 风格:始终使用深色半透明外壳,不随系统主题翻转
|
||||
ArrowButtonBackgroundBrush.Color = Color.FromArgb(204, 31, 31, 31);
|
||||
ContentBackgroundBrush.Color = Color.FromArgb(204, 31, 31, 31); // #CC1F1F1F
|
||||
ArrowPathFillBrush.Color = Colors.White;
|
||||
VolumeIconFillBrush.Color = Colors.White;
|
||||
VolumeIconFillBrush2.Color = Colors.White;
|
||||
VolumeValueForegroundBrush.Color = Color.FromArgb(230, 255, 255, 255);
|
||||
MagnifierTitleForegroundBrush.Color = Colors.White;
|
||||
MagnifierDescForegroundBrush.Color = Color.FromArgb(200, 255, 255, 255);
|
||||
Separator1BackgroundBrush.Color = Color.FromArgb(60, 255, 255, 255);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1873,6 +1871,46 @@ namespace Ink_Canvas.Windows
|
||||
|
||||
#endregion
|
||||
|
||||
#region 聚焦放大镜
|
||||
|
||||
private void MagnifierToggleButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (MagnifierWindow.HasInstance)
|
||||
{
|
||||
MagnifierWindow.HideInstance();
|
||||
MagnifierToggleButton.Content = "开启放大镜";
|
||||
}
|
||||
else
|
||||
{
|
||||
MagnifierWindow.Show((float)MagnifierZoomSlider.Value);
|
||||
if (MagnifierWindow.HasInstance)
|
||||
{
|
||||
MagnifierToggleButton.Content = "关闭放大镜";
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"切换聚焦放大镜失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void MagnifierZoomSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
if (MagnifierZoomValueText != null)
|
||||
{
|
||||
MagnifierZoomValueText.Text = $"{e.NewValue:0.0}x";
|
||||
}
|
||||
if (MagnifierWindow.HasInstance)
|
||||
{
|
||||
MagnifierWindow.SetZoom((float)e.NewValue);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公开方法
|
||||
|
||||
/// <summary>
|
||||
@@ -1881,6 +1919,12 @@ namespace Ink_Canvas.Windows
|
||||
public void UpdateVisibility(bool isInPPTMode)
|
||||
{
|
||||
Visibility = isInPPTMode ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (!isInPPTMode && MagnifierWindow.HasInstance)
|
||||
{
|
||||
MagnifierWindow.HideInstance();
|
||||
if (MagnifierToggleButton != null)
|
||||
MagnifierToggleButton.Content = "开启放大镜";
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -339,14 +339,12 @@ namespace Ink_Canvas.Windows.SettingsViews.Helpers
|
||||
};
|
||||
|
||||
bool isThemeRelated = false;
|
||||
string controlNameLower = controlName.ToLower();
|
||||
|
||||
foreach (var themeControl in themeRelatedControls)
|
||||
{
|
||||
string themeControlLower = themeControl.ToLower();
|
||||
if (controlNameLower.Contains(themeControlLower) ||
|
||||
themeControlLower.Contains(controlNameLower) ||
|
||||
controlNameLower == themeControlLower)
|
||||
// OrdinalIgnoreCase 避免在循环里反复 ToLower() 生成中间字符串。
|
||||
if (controlName.IndexOf(themeControl, StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
themeControl.IndexOf(controlName, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
isThemeRelated = true;
|
||||
break;
|
||||
|
||||
@@ -228,8 +228,14 @@ namespace Ink_Canvas.Windows.SettingsViews.Pages
|
||||
private void ViewboxFloatingBarScaleTransformValueSlider_ValueChanged(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!_isLoaded) return;
|
||||
var val = Math.Round(ViewboxFloatingBarScaleTransformValueSlider.Value, 2);
|
||||
ViewboxFloatingBarScaleTransformValueSlider.Value = val;
|
||||
var slider = ViewboxFloatingBarScaleTransformValueSlider;
|
||||
var val = Math.Round(slider.Value, 2);
|
||||
// 仅当四舍五入纠正了显示值时才回写;那次 set 会重入 ValueChanged 完成保存。
|
||||
if (slider.Value != val)
|
||||
{
|
||||
slider.Value = val;
|
||||
return;
|
||||
}
|
||||
SettingsManager.Settings.Appearance.ViewboxFloatingBarScaleTransformValue = val;
|
||||
SettingsManager.SaveSettingsToFile();
|
||||
var mw = GetMainWindow();
|
||||
@@ -247,8 +253,13 @@ namespace Ink_Canvas.Windows.SettingsViews.Pages
|
||||
private void ViewboxFloatingBarOpacityValueSlider_ValueChanged(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!_isLoaded) return;
|
||||
var val = Math.Round(ViewboxFloatingBarOpacityValueSlider.Value, 2);
|
||||
ViewboxFloatingBarOpacityValueSlider.Value = val;
|
||||
var slider = ViewboxFloatingBarOpacityValueSlider;
|
||||
var val = Math.Round(slider.Value, 2);
|
||||
if (slider.Value != val)
|
||||
{
|
||||
slider.Value = val;
|
||||
return;
|
||||
}
|
||||
SettingsManager.Settings.Appearance.ViewboxFloatingBarOpacityValue = val;
|
||||
SettingsManager.SaveSettingsToFile();
|
||||
var mw = GetMainWindow();
|
||||
@@ -258,8 +269,13 @@ namespace Ink_Canvas.Windows.SettingsViews.Pages
|
||||
private void ViewboxFloatingBarOpacityInPPTValueSlider_ValueChanged(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!_isLoaded) return;
|
||||
var val = Math.Round(ViewboxFloatingBarOpacityInPPTValueSlider.Value, 2);
|
||||
ViewboxFloatingBarOpacityInPPTValueSlider.Value = val;
|
||||
var slider = ViewboxFloatingBarOpacityInPPTValueSlider;
|
||||
var val = Math.Round(slider.Value, 2);
|
||||
if (slider.Value != val)
|
||||
{
|
||||
slider.Value = val;
|
||||
return;
|
||||
}
|
||||
SettingsManager.Settings.Appearance.ViewboxFloatingBarOpacityInPPTValue = val;
|
||||
SettingsManager.SaveSettingsToFile();
|
||||
var mw = GetMainWindow();
|
||||
@@ -290,8 +306,13 @@ namespace Ink_Canvas.Windows.SettingsViews.Pages
|
||||
private void ViewboxBlackBoardScaleTransformValueSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
if (!_isLoaded) return;
|
||||
var val = Math.Round(ViewboxBlackBoardScaleTransformValueSlider.Value, 2);
|
||||
ViewboxBlackBoardScaleTransformValueSlider.Value = val;
|
||||
var slider = ViewboxBlackBoardScaleTransformValueSlider;
|
||||
var val = Math.Round(slider.Value, 2);
|
||||
if (slider.Value != val)
|
||||
{
|
||||
slider.Value = val;
|
||||
return;
|
||||
}
|
||||
SettingsManager.Settings.Appearance.ViewboxBlackBoardScaleTransformValue = val;
|
||||
SettingsManager.SaveSettingsToFile();
|
||||
var mw = GetMainWindow();
|
||||
|
||||
@@ -373,30 +373,30 @@
|
||||
Toggled="ToggleSwitchSaveFullPageStrokes_Toggled"/>
|
||||
|
||||
<controls:LabeledSettingsCard x:Name="CardUseCustomSaveFileName"
|
||||
Header="使用自定义保存文件名"
|
||||
Description="开启后可选择保存文件的命名方式"
|
||||
Header="{i18n:I18n Key=Automation_UseCustomSaveFileName_Header}"
|
||||
Description="{i18n:I18n Key=Automation_UseCustomSaveFileName_Desc}"
|
||||
Icon="{x:Static ui:SegoeFluentIcons.Edit}"
|
||||
Toggled="ToggleSwitchUseCustomSaveFileName_Toggled"/>
|
||||
|
||||
<ui:SettingsExpander x:Name="CardSaveFileNamePreset" Header="文件名格式">
|
||||
<ui:SettingsExpander x:Name="CardSaveFileNamePreset" Header="{i18n:I18n Key=Automation_SaveFileNameFormat}">
|
||||
<ui:SettingsExpander.HeaderIcon>
|
||||
<ui:FontIcon Icon="{x:Static ui:SegoeFluentIcons.Edit}"/>
|
||||
</ui:SettingsExpander.HeaderIcon>
|
||||
<ComboBox x:Name="ComboBoxSaveFileNamePreset"
|
||||
MinWidth="260"
|
||||
SelectionChanged="ComboBoxSaveFileNamePreset_SelectionChanged">
|
||||
<ComboBoxItem Content="时间戳(默认)" Tag="{}{datetime}"/>
|
||||
<ComboBoxItem Content="日期" Tag="{}{date}"/>
|
||||
<ComboBoxItem Content="日期 + 时间" Tag="{}{date}_{time}"/>
|
||||
<ComboBoxItem Content="日期 + 模式" Tag="{}{date}_{mode}"/>
|
||||
<ComboBoxItem Content="日期 + 模式 + 页码" Tag="{}{date}_{mode}_Page-{page}"/>
|
||||
<ComboBoxItem Content="日期 + 模式 + 页码 + 笔画数" Tag="{}{date}_{mode}_Page-{page}_Strokes-{count}"/>
|
||||
<ComboBoxItem Content="自定义..." Tag="__custom__"/>
|
||||
<ComboBoxItem Content="{i18n:I18n Key=Automation_SaveFileName_Timestamp}" Tag="{}{datetime}"/>
|
||||
<ComboBoxItem Content="{i18n:I18n Key=Automation_SaveFileName_Date}" Tag="{}{date}"/>
|
||||
<ComboBoxItem Content="{i18n:I18n Key=Automation_SaveFileName_DateTime}" Tag="{}{date}_{time}"/>
|
||||
<ComboBoxItem Content="{i18n:I18n Key=Automation_SaveFileName_DateMode}" Tag="{}{date}_{mode}"/>
|
||||
<ComboBoxItem Content="{i18n:I18n Key=Automation_SaveFileName_DateModePage}" Tag="{}{date}_{mode}_Page-{page}"/>
|
||||
<ComboBoxItem Content="{i18n:I18n Key=Automation_SaveFileName_DateModePageCount}" Tag="{}{date}_{mode}_Page-{page}_Strokes-{count}"/>
|
||||
<ComboBoxItem Content="{i18n:I18n Key=Automation_SaveFileName_Custom}" Tag="__custom__"/>
|
||||
</ComboBox>
|
||||
<ui:SettingsExpander.Items>
|
||||
<ui:SettingsCard x:Name="CardCustomSaveFileNameTemplate"
|
||||
Header="自定义模板"
|
||||
Description="可用占位符:{date} {time} {datetime} {mode} {page} {count} {type}"
|
||||
Header="{i18n:I18n Key=Automation_SaveFileName_CustomTemplate_Header}"
|
||||
Description="{i18n:I18n Key=Automation_SaveFileName_CustomTemplate_Desc}"
|
||||
Visibility="Collapsed">
|
||||
<TextBox x:Name="TextBoxCustomSaveFileNameTemplate"
|
||||
MinWidth="260"
|
||||
|
||||
@@ -294,8 +294,14 @@ namespace Ink_Canvas.Windows.SettingsViews.Pages
|
||||
private void BrushAutoRestoreWidthSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
if (!_isLoaded) return;
|
||||
var val = Math.Round(BrushAutoRestoreWidthSlider.Value, 2);
|
||||
BrushAutoRestoreWidthSlider.Value = val;
|
||||
var slider = BrushAutoRestoreWidthSlider;
|
||||
var val = Math.Round(slider.Value, 2);
|
||||
// 仅在四舍五入纠正了显示值时回写;那次 set 会重入 ValueChanged 完成保存。
|
||||
if (slider.Value != val)
|
||||
{
|
||||
slider.Value = val;
|
||||
return;
|
||||
}
|
||||
SettingsManager.Settings.Canvas.BrushAutoRestoreWidth = val;
|
||||
SettingsManager.SaveSettingsToFile();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:controls="clr-namespace:Ink_Canvas.Controls;assembly=InkCanvas.Controls"
|
||||
xmlns:i18n="clr-namespace:Ink_Canvas.MarkupExtensions"
|
||||
mc:Ignorable="d"
|
||||
Title="Debug">
|
||||
|
||||
@@ -32,8 +33,8 @@
|
||||
Text="Debug" />
|
||||
|
||||
<controls:LabeledSettingsCard x:Name="ToggleSwitchDebugConsole"
|
||||
Header="显示调试窗口"
|
||||
Description="显示一个独立的控制台窗口,用于实时输出日志(开启后立即生效;关闭设置中的“启用日志记录”将不会输出内容)。"
|
||||
Header="{i18n:I18n Key=Debug_ShowConsole_Header}"
|
||||
Description="{i18n:I18n Key=Debug_ShowConsole_Desc}"
|
||||
Icon="{x:Static ui:SegoeFluentIcons.DeveloperTools}"
|
||||
IsOn="False"
|
||||
Toggled="ToggleSwitchDebugConsole_Toggled"/>
|
||||
|
||||
@@ -216,8 +216,14 @@ namespace Ink_Canvas.Windows.SettingsViews.Pages
|
||||
private void MLAvoidanceWeightSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
if (!_isLoaded) return;
|
||||
var val = Math.Round(MLAvoidanceWeightSlider.Value, 2);
|
||||
MLAvoidanceWeightSlider.Value = val;
|
||||
var slider = MLAvoidanceWeightSlider;
|
||||
var val = Math.Round(slider.Value, 2);
|
||||
// 仅当四舍五入纠正了显示值时才回写;那次 set 会重入 ValueChanged 完成保存。
|
||||
if (slider.Value != val)
|
||||
{
|
||||
slider.Value = val;
|
||||
return;
|
||||
}
|
||||
SettingsManager.Settings.RandSettings.MLAvoidanceWeight = val;
|
||||
SettingsManager.SaveSettingsToFile();
|
||||
}
|
||||
@@ -281,8 +287,13 @@ namespace Ink_Canvas.Windows.SettingsViews.Pages
|
||||
private void TimerVolumeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
if (!_isLoaded) return;
|
||||
var val = Math.Round(TimerVolumeSlider.Value, 2);
|
||||
TimerVolumeSlider.Value = val;
|
||||
var slider = TimerVolumeSlider;
|
||||
var val = Math.Round(slider.Value, 2);
|
||||
if (slider.Value != val)
|
||||
{
|
||||
slider.Value = val;
|
||||
return;
|
||||
}
|
||||
SettingsManager.Settings.RandSettings.TimerVolume = val;
|
||||
SettingsManager.SaveSettingsToFile();
|
||||
}
|
||||
@@ -321,8 +332,13 @@ namespace Ink_Canvas.Windows.SettingsViews.Pages
|
||||
private void ProgressiveReminderVolumeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
if (!_isLoaded) return;
|
||||
var val = Math.Round(ProgressiveReminderVolumeSlider.Value, 2);
|
||||
ProgressiveReminderVolumeSlider.Value = val;
|
||||
var slider = ProgressiveReminderVolumeSlider;
|
||||
var val = Math.Round(slider.Value, 2);
|
||||
if (slider.Value != val)
|
||||
{
|
||||
slider.Value = val;
|
||||
return;
|
||||
}
|
||||
SettingsManager.Settings.RandSettings.ProgressiveReminderVolume = val;
|
||||
SettingsManager.SaveSettingsToFile();
|
||||
}
|
||||
|
||||
@@ -155,9 +155,9 @@
|
||||
</ui:NavigationViewItem>
|
||||
<ui:NavigationViewItem
|
||||
x:Name="SecurityPageItem"
|
||||
Content="安全"
|
||||
Content="{i18n:I18n Key=Settings_Nav_Security}"
|
||||
Tag="SecurityPage"
|
||||
ToolTipService.ToolTip="安全密码与进程保护">
|
||||
ToolTipService.ToolTip="{i18n:I18n Key=Settings_Nav_Security_Tooltip}">
|
||||
<ui:NavigationViewItem.Icon>
|
||||
<ui:FontIcon Icon="{x:Static ui:SegoeFluentIcons.Permissions}"/>
|
||||
</ui:NavigationViewItem.Icon>
|
||||
@@ -195,9 +195,9 @@
|
||||
</ui:NavigationViewItem>
|
||||
<ui:NavigationViewItem
|
||||
x:Name="HotkeyPageItem"
|
||||
Content="快捷键"
|
||||
Content="{i18n:I18n Key=Settings_Nav_Hotkey}"
|
||||
Tag="HotkeyPage"
|
||||
ToolTipService.ToolTip="快捷键设置">
|
||||
ToolTipService.ToolTip="{i18n:I18n Key=Settings_Nav_Hotkey_Tooltip}">
|
||||
<ui:NavigationViewItem.Icon>
|
||||
<ui:FontIcon Icon="{x:Static ui:SegoeFluentIcons.KeyboardStandard}"/>
|
||||
</ui:NavigationViewItem.Icon>
|
||||
|
||||
@@ -587,10 +587,9 @@ namespace Ink_Canvas.Windows.SettingsViews
|
||||
if (string.IsNullOrWhiteSpace(raw)) return;
|
||||
|
||||
string query = raw.Trim();
|
||||
string queryLower = query.ToLower();
|
||||
|
||||
var entry = _searchIndex.FirstOrDefault(e => e.Text.Equals(query, StringComparison.OrdinalIgnoreCase))
|
||||
?? _searchIndex.FirstOrDefault(e => e.Text.ToLower().Contains(queryLower));
|
||||
?? _searchIndex.FirstOrDefault(e => e.Text.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
|
||||
NavigateToSearchEntry(entry);
|
||||
}
|
||||
@@ -608,9 +607,8 @@ namespace Ink_Canvas.Windows.SettingsViews
|
||||
return;
|
||||
}
|
||||
|
||||
string queryLower = query.ToLower();
|
||||
var suggestions = _searchIndex
|
||||
.Where(e => e.Text.ToLower().Contains(queryLower))
|
||||
.Where(e => e.Text.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
.Select(e => e.Text)
|
||||
.Distinct()
|
||||
.Take(50)
|
||||
|
||||
@@ -349,10 +349,68 @@ namespace Ink_Canvas.Windows
|
||||
ThemeHelper.ApplyTheme(this, settings, theme =>
|
||||
{
|
||||
if (theme == "Dark") SetDarkThemeBorder();
|
||||
UpdateDigitDisplays();
|
||||
// 复用当前状态下的显示逻辑,避免把暂停/超时/运行中的读数重置为初始设定时间。
|
||||
RefreshDigitDisplayForCurrentState();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 依据当前计时器状态刷新数字显示:
|
||||
/// 运行中按 DateTime.Now、暂停中按 pauseTime 推算;处于超时模式时按超时值渲染;未启动时回退到初始设定值。
|
||||
/// 供主题切换等场景直接复用 Timer_Elapsed 的渲染分支。
|
||||
/// </summary>
|
||||
private void RefreshDigitDisplayForCurrentState()
|
||||
{
|
||||
if (!isTimerRunning)
|
||||
{
|
||||
UpdateDigitDisplays();
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime referenceTime = isPaused ? pauseTime : DateTime.Now;
|
||||
TimeSpan timeSpan = referenceTime - startTime;
|
||||
TimeSpan totalTimeSpan = new TimeSpan(hour, minute, second);
|
||||
|
||||
if (!isOvertimeMode)
|
||||
{
|
||||
TimeSpan leftTimeSpan = totalTimeSpan - timeSpan;
|
||||
if (leftTimeSpan.Milliseconds > 0) leftTimeSpan += new TimeSpan(0, 0, 1);
|
||||
if (leftTimeSpan < TimeSpan.Zero) leftTimeSpan = TimeSpan.Zero;
|
||||
|
||||
int displayHours = Math.Min(99, (int)leftTimeSpan.TotalHours);
|
||||
SetDigitDisplay("Digit1Display", displayHours / 10);
|
||||
SetDigitDisplay("Digit2Display", displayHours % 10);
|
||||
SetDigitDisplay("Digit3Display", leftTimeSpan.Minutes / 10);
|
||||
SetDigitDisplay("Digit4Display", leftTimeSpan.Minutes % 10);
|
||||
SetDigitDisplay("Digit5Display", leftTimeSpan.Seconds / 10);
|
||||
SetDigitDisplay("Digit6Display", leftTimeSpan.Seconds % 10);
|
||||
SetColonDisplay(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
TimeSpan overtimeSpan = timeSpan - totalTimeSpan;
|
||||
if (overtimeSpan < TimeSpan.Zero) overtimeSpan = TimeSpan.Zero;
|
||||
|
||||
int displayHours = Math.Max(0, Math.Min(99, (int)overtimeSpan.TotalHours));
|
||||
bool shouldShowRed = MainWindow.Settings?.RandSettings?.EnableOvertimeRedText == true;
|
||||
|
||||
int hoursTens = Math.Max(0, Math.Min(9, Math.Abs(displayHours / 10) % 10));
|
||||
int hoursOnes = Math.Max(0, Math.Min(9, (displayHours % 10 + 10) % 10));
|
||||
int minutesTens = Math.Max(0, Math.Min(9, Math.Abs(overtimeSpan.Minutes) / 10));
|
||||
int minutesOnes = Math.Max(0, Math.Min(9, Math.Abs(overtimeSpan.Minutes) % 10));
|
||||
int secondsTens = Math.Max(0, Math.Min(9, Math.Abs(overtimeSpan.Seconds) / 10));
|
||||
int secondsOnes = Math.Max(0, Math.Min(9, Math.Abs(overtimeSpan.Seconds) % 10));
|
||||
|
||||
SetDigitDisplay("Digit1Display", hoursTens, shouldShowRed);
|
||||
SetDigitDisplay("Digit2Display", hoursOnes, shouldShowRed);
|
||||
SetDigitDisplay("Digit3Display", minutesTens, shouldShowRed);
|
||||
SetDigitDisplay("Digit4Display", minutesOnes, shouldShowRed);
|
||||
SetDigitDisplay("Digit5Display", secondsTens, shouldShowRed);
|
||||
SetDigitDisplay("Digit6Display", secondsOnes, shouldShowRed);
|
||||
SetColonDisplay(shouldShowRed);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDigitDisplays()
|
||||
{
|
||||
SetDigitDisplay("Digit1Display", hour / 10);
|
||||
|
||||
-1
@@ -11,7 +11,6 @@
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="PresentationCore" />
|
||||
<Reference Include="PresentationFramework" />
|
||||
+1
-1
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace InkCanvasForClass.IACoreHelper
|
||||
namespace InkCanvas.IACoreHelper
|
||||
{
|
||||
// Named Pipe 名称,主进程和辅助进程共用
|
||||
internal static class IpcConstants
|
||||
@@ -5,7 +5,7 @@ using System.Linq;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace InkCanvasForClass.IACoreHelper
|
||||
namespace InkCanvas.IACoreHelper
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("InkCanvasForClass.IACoreHelper")]
|
||||
[assembly: AssemblyTitle("InkCanvas.IACoreHelper")]
|
||||
[assembly: AssemblyDescription("IACore 32-bit ink shape recognition helper process")]
|
||||
[assembly: AssemblyCompany("ICC CE")]
|
||||
[assembly: AssemblyProduct("InkCanvasForClass.IACoreHelper")]
|
||||
[assembly: AssemblyProduct("InkCanvas.IACoreHelper")]
|
||||
[assembly: AssemblyCopyright("Copyright © ICC CE")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
Reference in New Issue
Block a user