diff --git a/Ink Canvas.sln b/Ink Canvas.sln index 0daac4d2..9e46f5cd 100644 --- a/Ink Canvas.sln +++ b/Ink Canvas.sln @@ -1,3 +1,4 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.33530.505 diff --git a/Ink Canvas/App.xaml b/Ink Canvas/App.xaml index 39bb6117..9ed51200 100644 --- a/Ink Canvas/App.xaml +++ b/Ink Canvas/App.xaml @@ -1,8 +1,9 @@ - @@ -32,10 +33,30 @@ + + + + + + + + + + + + + + + + + + + + - + @@ -242,7 +263,7 @@ ToolTipText="InkCanvasForClass" ContextMenu="{StaticResource SysTrayMenu}" IconSource="/Resources/icc.ico"/> - + diff --git a/Ink Canvas/App.xaml.cs b/Ink Canvas/App.xaml.cs index 787cbc0b..2238c408 100644 --- a/Ink Canvas/App.xaml.cs +++ b/Ink Canvas/App.xaml.cs @@ -1,4 +1,4 @@ -using Hardcodet.Wpf.TaskbarNotification; +using H.NotifyIcon; using Ink_Canvas.Helpers; using Ink_Canvas.Properties; using iNKORE.UI.WPF.Modern.Controls; @@ -1061,6 +1061,7 @@ namespace Ink_Canvas } _taskbar = (TaskbarIcon)FindResource("TaskbarTrayIcon"); + _taskbar.ForceCreate(); StartArgs = e.Args; diff --git a/Ink Canvas/Controls/CopyButton.xaml b/Ink Canvas/Controls/CopyButton.xaml new file mode 100644 index 00000000..fe3bff2e --- /dev/null +++ b/Ink Canvas/Controls/CopyButton.xaml @@ -0,0 +1,24 @@ + + + diff --git a/Ink Canvas/Controls/CopyButton.xaml.cs b/Ink Canvas/Controls/CopyButton.xaml.cs new file mode 100644 index 00000000..fc2c2f30 --- /dev/null +++ b/Ink Canvas/Controls/CopyButton.xaml.cs @@ -0,0 +1,115 @@ +using System; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Animation; + +namespace Ink_Canvas.Controls +{ + public partial class CopyButton : UserControl + { + public static readonly DependencyProperty TextProperty = DependencyProperty.Register( + nameof(Text), typeof(string), typeof(CopyButton), new PropertyMetadata(string.Empty)); + + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + public event EventHandler Click; + + public CopyButton() + { + InitializeComponent(); + } + + private void CopyButton_Click(object sender, RoutedEventArgs e) + { + try + { + if (!string.IsNullOrEmpty(Text)) + { + Clipboard.SetText(Text); + } + + ShowSuccessAnimation(); + Click?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + MessageBox.Show(ex.ToString(), "Unable to Perform Copy", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private async void ShowSuccessAnimation() + { + var copyScaleAnim = new DoubleAnimation + { + To = 0, + Duration = TimeSpan.FromMilliseconds(150) + }; + ScaleTransform_Copy.BeginAnimation(ScaleTransform.ScaleXProperty, copyScaleAnim); + + var copyOpacityAnim = new DoubleAnimation + { + To = 0, + BeginTime = TimeSpan.FromMilliseconds(100), + Duration = TimeSpan.FromMilliseconds(10) + }; + FontIcon_Copy.BeginAnimation(UIElement.OpacityProperty, copyOpacityAnim); + + await Task.Delay(150); + var successScaleAnim = new DoubleAnimation + { + To = 1, + Duration = TimeSpan.FromMilliseconds(150), + EasingFunction = new BackEase { EasingMode = EasingMode.EaseOut, Amplitude = 0.2 } + }; + ScaleTransform_Success.BeginAnimation(ScaleTransform.ScaleXProperty, successScaleAnim); + + var successOpacityAnim = new DoubleAnimation + { + To = 1, + Duration = TimeSpan.FromMilliseconds(15) + }; + FontIcon_Success.BeginAnimation(UIElement.OpacityProperty, successOpacityAnim); + + await Task.Delay(1000); + ShowCopyAnimation(); + } + + private async void ShowCopyAnimation() + { + var successOpacityAnim = new DoubleAnimation + { + To = 0, + Duration = TimeSpan.FromMilliseconds(150) + }; + FontIcon_Success.BeginAnimation(UIElement.OpacityProperty, successOpacityAnim); + + await Task.Delay(150); + var copyScaleAnim = new DoubleAnimation + { + To = 1, + Duration = TimeSpan.Zero + }; + ScaleTransform_Copy.BeginAnimation(ScaleTransform.ScaleXProperty, copyScaleAnim); + + var copyOpacityAnim = new DoubleAnimation + { + To = 1, + Duration = TimeSpan.FromMilliseconds(150) + }; + FontIcon_Copy.BeginAnimation(UIElement.OpacityProperty, copyOpacityAnim); + + var successScaleAnim = new DoubleAnimation + { + To = 0, + Duration = TimeSpan.Zero + }; + ScaleTransform_Success.BeginAnimation(ScaleTransform.ScaleXProperty, successScaleAnim); + } + } +} diff --git a/Ink Canvas/Controls/PdfEmbeddedView.cs b/Ink Canvas/Controls/PdfEmbeddedView.cs new file mode 100644 index 00000000..6c4d21f7 --- /dev/null +++ b/Ink Canvas/Controls/PdfEmbeddedView.cs @@ -0,0 +1,145 @@ +using Ink_Canvas.Helpers; +using System; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace Ink_Canvas.Controls +{ + /// + /// 画布上的多页 PDF:仅显示当前页;翻页与页码由主窗口 PDF 侧栏控制(无 XAML 文件)。 + /// + public class PdfEmbeddedView : UserControl + { + private readonly Image _pageImage; + + private string _pdfPath; + private uint _pageCount; + private uint _currentIndex; + private bool _compressLargePictures; + private bool _isPagingBusy; + private bool _layoutSizeCommitted; + + /// 页码或可翻页状态变化(用于更新侧栏)。 + public event EventHandler PageNavigationStateChanged; + + public PdfEmbeddedView() + { + MinWidth = 80; + MinHeight = 60; + + var grid = new Grid { ClipToBounds = true }; + _pageImage = new Image + { + Stretch = Stretch.Uniform, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + grid.Children.Add(_pageImage); + Content = grid; + } + + /// + /// 初始化并显示指定页;由 MainWindow 在 UI 线程创建后调用。 + /// + /// 从 0 开始的页码,超出范围时夹紧到合法区间。 + public async Task InitializeAsync(string pdfFilePath, uint pageCount, bool compressLargePictures, uint initialPageIndex = 0) + { + _pdfPath = pdfFilePath ?? throw new ArgumentNullException(nameof(pdfFilePath)); + _pageCount = pageCount; + _compressLargePictures = compressLargePictures; + if (_pageCount == 0) + _currentIndex = 0; + else + _currentIndex = initialPageIndex >= _pageCount ? _pageCount - 1 : initialPageIndex; + + await ShowPageAsync(_currentIndex); + } + + public string PdfPath => _pdfPath; + + public uint PageCount => _pageCount; + + public uint CurrentPageIndex => _currentIndex; + + public string PageLabelText => _pageCount == 0 ? "" : $"{_currentIndex + 1} / {_pageCount}"; + + public bool CanGoPrevious => !_isPagingBusy && _pageCount > 1 && _currentIndex > 0; + + public bool CanGoNext => !_isPagingBusy && _pageCount > 1 && _currentIndex < _pageCount - 1; + + public async Task GoToPreviousPageAsync() + { + await GoRelativeAsync(-1); + } + + public async Task GoToNextPageAsync() + { + await GoRelativeAsync(1); + } + + private void NotifyPageNavigationStateChanged() + { + PageNavigationStateChanged?.Invoke(this, EventArgs.Empty); + } + + private async Task GoRelativeAsync(int delta) + { + if (_isPagingBusy || _pageCount <= 1) + return; + int next = (int)_currentIndex + delta; + if (next < 0 || next >= _pageCount) + return; + _currentIndex = (uint)next; + await ShowPageAsync(_currentIndex); + } + + private async Task ShowPageAsync(uint pageIndex) + { + _isPagingBusy = true; + NotifyPageNavigationStateChanged(); + try + { + BitmapSource raw = await PdfWinRtHelper.RenderPageToBitmapSourceAsync(_pdfPath, pageIndex); + if (raw == null) + return; + + BitmapSource display = ApplyCompressionIfNeeded(raw); + _pageImage.Source = display; + if (!_layoutSizeCommitted) + { + bool callerSized = !double.IsNaN(Width) && Width > 0 && !double.IsNaN(Height) && Height > 0; + if (!callerSized) + { + Width = display.PixelWidth; + Height = display.PixelHeight; + } + + _layoutSizeCommitted = true; + } + } + finally + { + _isPagingBusy = false; + NotifyPageNavigationStateChanged(); + } + } + + private BitmapSource ApplyCompressionIfNeeded(BitmapSource rendered) + { + int width = rendered.PixelWidth; + int height = rendered.PixelHeight; + if (_compressLargePictures && (width > 1920 || height > 1080)) + { + double scaleX = 1920.0 / width; + double scaleY = 1080.0 / height; + double scale = Math.Min(scaleX, scaleY); + return new TransformedBitmap(rendered, new ScaleTransform(scale, scale)); + } + + return rendered; + } + } +} diff --git a/Ink Canvas/Helpers/GlobalHotkeyManager.cs b/Ink Canvas/Helpers/GlobalHotkeyManager.cs index e9bfcd97..e0516650 100644 --- a/Ink Canvas/Helpers/GlobalHotkeyManager.cs +++ b/Ink Canvas/Helpers/GlobalHotkeyManager.cs @@ -285,6 +285,7 @@ namespace Ink_Canvas.Helpers // 功能快捷键 RegisterHotkey("DrawLine", Key.L, ModifierKeys.Alt, () => _mainWindow.BtnDrawLine_Click(null, null)); RegisterHotkey("Screenshot", Key.C, ModifierKeys.Alt, () => _mainWindow.SaveScreenShotToDesktop()); + RegisterHotkey("QuickDraw", Key.K, ModifierKeys.Alt, () => _mainWindow.OpenQuickDrawFromHotkey()); RegisterHotkey("Hide", Key.V, ModifierKeys.Alt, () => _mainWindow.SymbolIconEmoji_MouseUp(null, null)); // 退出快捷键 @@ -1033,6 +1034,7 @@ namespace Ink_Canvas.Helpers new HotkeyConfigItem { Name = "Pen5", Key = Key.D5, Modifiers = ModifierKeys.Alt }, new HotkeyConfigItem { Name = "DrawLine", Key = Key.L, Modifiers = ModifierKeys.Alt }, new HotkeyConfigItem { Name = "Screenshot", Key = Key.C, Modifiers = ModifierKeys.Alt }, + new HotkeyConfigItem { Name = "QuickDraw", Key = Key.K, Modifiers = ModifierKeys.Alt }, new HotkeyConfigItem { Name = "Hide", Key = Key.V, Modifiers = ModifierKeys.Alt }, new HotkeyConfigItem { Name = "Exit", Key = Key.Escape, Modifiers = ModifierKeys.None } }); @@ -1111,6 +1113,14 @@ namespace Ink_Canvas.Helpers } } + // 旧版 HotkeyConfig.json 无「快抽」项时补注册默认组合,避免升级后无快捷键 + if (successCount > 0 && !IsHotkeyRegistered("QuickDraw")) + { + var quickDrawAction = GetActionByName("QuickDraw"); + if (quickDrawAction != null && RegisterHotkey("QuickDraw", Key.K, ModifierKeys.Alt, quickDrawAction)) + successCount++; + } + if (successCount > 0) { _hotkeysShouldBeRegistered = true; @@ -1221,6 +1231,8 @@ namespace Ink_Canvas.Helpers return () => _mainWindow.BtnDrawLine_Click(null, null); case "Screenshot": return () => _mainWindow.SaveScreenShotToDesktop(); + case "QuickDraw": + return () => _mainWindow.OpenQuickDrawFromHotkey(); case "Hide": return () => _mainWindow.SymbolIconEmoji_MouseUp(null, null); case "Exit": diff --git a/Ink Canvas/Helpers/InkRecognizeHelper.cs b/Ink Canvas/Helpers/InkRecognizeHelper.cs index 7ec48cf3..d283aaa5 100644 --- a/Ink Canvas/Helpers/InkRecognizeHelper.cs +++ b/Ink Canvas/Helpers/InkRecognizeHelper.cs @@ -1,4 +1,3 @@ -using Ink_Canvas; using System.Linq; using System.Threading.Tasks; using System.Windows; @@ -49,13 +48,17 @@ namespace Ink_Canvas.Helpers break; } if (alternates.Count > 0) - analysisAlternate = alternates[0]; + { + var altFinal = alternates[0]; + if (altFinal?.AlternateNodes != null && altFinal.AlternateNodes.Count > 0) + analysisAlternate = altFinal; + } } } analyzer.Dispose(); - if (analysisAlternate != null && analysisAlternate.AlternateNodes.Count > 0) + if (analysisAlternate != null && analysisAlternate.AlternateNodes != null && analysisAlternate.AlternateNodes.Count > 0) { var node = analysisAlternate.AlternateNodes[0] as InkDrawingNode; if (node == null) diff --git a/Ink Canvas/Helpers/PPTManager.cs b/Ink Canvas/Helpers/PPTManager.cs index 86922667..a02c3371 100644 --- a/Ink Canvas/Helpers/PPTManager.cs +++ b/Ink Canvas/Helpers/PPTManager.cs @@ -390,11 +390,15 @@ namespace Ink_Canvas.Helpers { try { - PPTApplication.PresentationOpen -= OnPresentationOpen; - PPTApplication.PresentationClose -= OnPresentationClose; - PPTApplication.SlideShowBegin -= OnSlideShowBegin; - PPTApplication.SlideShowNextSlide -= OnSlideShowNextSlide; - PPTApplication.SlideShowEnd -= OnSlideShowEnd; + // 再次检查PPTApplication是否为null,因为可能在异步操作期间被修改 + if (PPTApplication != null && Marshal.IsComObject(PPTApplication)) + { + PPTApplication.PresentationOpen -= OnPresentationOpen; + PPTApplication.PresentationClose -= OnPresentationClose; + PPTApplication.SlideShowBegin -= OnSlideShowBegin; + PPTApplication.SlideShowNextSlide -= OnSlideShowNextSlide; + PPTApplication.SlideShowEnd -= OnSlideShowEnd; + } } catch (COMException comEx) { diff --git a/Ink Canvas/Helpers/PdfWinRtHelper.cs b/Ink Canvas/Helpers/PdfWinRtHelper.cs new file mode 100644 index 00000000..58a17617 --- /dev/null +++ b/Ink Canvas/Helpers/PdfWinRtHelper.cs @@ -0,0 +1,79 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media.Imaging; +using Windows.Data.Pdf; +using Windows.Storage; +using Windows.Storage.Streams; + +namespace Ink_Canvas.Helpers +{ + /// + /// 使用 Windows.Data.Pdf(WinRT)将 PDF 页渲染为 WPF 可用的位图。 + /// + internal static class PdfWinRtHelper + { + public static async Task GetPageCountAsync(string pdfPath) + { + if (string.IsNullOrWhiteSpace(pdfPath) || !File.Exists(pdfPath)) + return 0; + + var file = await StorageFile.GetFileFromPathAsync(pdfPath).AsTask(); + var doc = await PdfDocument.LoadFromFileAsync(file).AsTask(); + if (doc.IsPasswordProtected) + return 0; + return doc.PageCount; + } + + public static async Task RenderPageToBitmapSourceAsync(string pdfPath, uint pageIndex) + { + if (string.IsNullOrWhiteSpace(pdfPath) || !File.Exists(pdfPath)) + return null; + + var file = await StorageFile.GetFileFromPathAsync(pdfPath).AsTask(); + var doc = await PdfDocument.LoadFromFileAsync(file).AsTask(); + if (doc.IsPasswordProtected) + return null; + if (pageIndex >= doc.PageCount) + return null; + + var page = doc.GetPage(pageIndex); + try + { + using (var ras = new InMemoryRandomAccessStream()) + { + await page.RenderToStreamAsync(ras).AsTask(); + ras.Seek(0); + + var ms = new MemoryStream(); + using (var netStream = ras.AsStreamForRead()) + netStream.CopyTo(ms); + ms.Position = 0; + + try + { + return await Application.Current.Dispatcher.InvokeAsync(() => + { + var bi = new BitmapImage(); + bi.BeginInit(); + bi.StreamSource = ms; + bi.CacheOption = BitmapCacheOption.OnLoad; + bi.EndInit(); + bi.Freeze(); + return (BitmapSource)bi; + }); + } + finally + { + ms.Dispose(); + } + } + } + finally + { + (page as IDisposable)?.Dispose(); + } + } + } +} diff --git a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherButton.cs b/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherButton.cs deleted file mode 100644 index 479387db..00000000 --- a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherButton.cs +++ /dev/null @@ -1,274 +0,0 @@ -using iNKORE.UI.WPF.Controls; -using System; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Media; - -namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher -{ - /// - /// 启动台按钮控件 - /// - public class LauncherButton - { - /// - /// 父插件 - /// - private readonly SuperLauncherPlugin _plugin; - - /// - /// 实际按钮控件 - /// - private readonly SimpleStackPanel _panel; - - /// - /// 获取按钮UI元素 - /// - public UIElement Element => _panel; - - /// - /// 构造函数 - /// - /// 父插件 - public LauncherButton(SuperLauncherPlugin plugin) - { - try - { - _plugin = plugin; - LogHelper.WriteLogToFile("开始创建启动台按钮"); - - // 创建SimpleStackPanel - _panel = new SimpleStackPanel - { - Name = "Launcher_Icon", - Orientation = Orientation.Vertical, - HorizontalAlignment = HorizontalAlignment.Center, - Width = 28, - Margin = new Thickness(0, -2, 0, 0), - Background = Brushes.Transparent - }; - - LogHelper.WriteLogToFile("创建SimpleStackPanel完成"); - - // 添加图标 - var image = CreateIconImage(); - _panel.Children.Add(image); - - // 添加文本 - TextBlock textBlock = new TextBlock - { - Text = "启动台", - Foreground = Brushes.Black, - FontSize = 8, - Margin = new Thickness(0, 1, 0, 0), - TextAlignment = TextAlignment.Center - }; - _panel.Children.Add(textBlock); - - // 设置鼠标事件 - _panel.MouseDown += Panel_MouseDown; - _panel.MouseUp += Panel_MouseUp; - _panel.MouseLeave += Panel_MouseLeave; - - // 右键菜单支持 - _panel.ContextMenu = CreateContextMenu(); - - // 设置工具提示 - _panel.ToolTip = "启动台"; - - LogHelper.WriteLogToFile("启动台按钮创建完成"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"创建启动台按钮时出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - } - } - - /// - /// 创建右键菜单 - /// - private ContextMenu CreateContextMenu() - { - try - { - // 创建菜单 - ContextMenu menu = new ContextMenu(); - - // 创建位置切换菜单项 - MenuItem positionMenuItem = new MenuItem(); - positionMenuItem.Header = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left ? - "移至右侧" : "移至左侧"; - positionMenuItem.Click += (s, e) => - { - // 切换位置 - _plugin.Config.ButtonPosition = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left ? - LauncherButtonPosition.Right : LauncherButtonPosition.Left; - - // 更新按钮位置 - _plugin.UpdateButtonPosition(); - - // 保存配置 - _plugin.SaveConfig(); - - LogHelper.WriteLogToFile($"通过右键菜单切换启动台按钮位置为: {_plugin.Config.ButtonPosition}"); - }; - menu.Items.Add(positionMenuItem); - - // 添加设置菜单项 - MenuItem settingsMenuItem = new MenuItem(); - settingsMenuItem.Header = "打开设置"; - settingsMenuItem.Click += (s, e) => - { - // 打开插件设置窗口 - var mainWindow = Application.Current.MainWindow; - if (mainWindow != null) - { - try - { - // 使用反射调用主窗口的ShowPluginSettings方法 - var method = mainWindow.GetType().GetMethod("ShowPluginSettings"); - if (method != null) - { - method.Invoke(mainWindow, null); - LogHelper.WriteLogToFile("已打开插件设置窗口"); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"打开插件设置窗口失败: {ex.Message}", LogHelper.LogType.Error); - } - } - }; - menu.Items.Add(settingsMenuItem); - - return menu; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"创建右键菜单时出错: {ex.Message}", LogHelper.LogType.Error); - return null; - } - } - - /// - /// 获取实际的UI元素 - /// - [Obsolete("使用Element属性代替")] - public UIElement GetUIElement() - { - return _panel; - } - - /// - /// 创建图标图像 - /// - private Image CreateIconImage() - { - try - { - // 创建图像 - Image image = new Image - { - Height = 17, - Margin = new Thickness(0, 3, 0, 0) - }; - - // 设置位图缩放模式 - RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality); - - // 创建绘图图像 - DrawingImage drawingImage = new DrawingImage(); - DrawingGroup drawingGroup = new DrawingGroup(); - drawingGroup.ClipGeometry = Geometry.Parse("M0,0 V24 H24 V0 H0 Z"); - - // 使用提供的应用网格图标 - GeometryDrawing geometryDrawing = new GeometryDrawing - { - Brush = new SolidColorBrush(Color.FromRgb(0x1B, 0x1B, 0x1B)), - Geometry = Geometry.Parse("F0 M24,24z M0,0z M4.41721,4.29873C4.35178,4.29873,4.29873,4.35178,4.29873,4.41721L4.29873,9.15646C4.29873,9.22189,4.35178,9.27494,4.41721,9.27494L9.15646,9.27494C9.22189,9.27494,9.27494,9.22189,9.27494,9.15646L9.27494,4.41721C9.27494,4.35178,9.22189,4.29873,9.15646,4.29873L4.41721,4.29873z M2.64,4.41721C2.64,3.43569,3.43569,2.64,4.41721,2.64L9.15646,2.64C10.138,2.64,10.9337,3.43569,10.9337,4.41721L10.9337,9.15646C10.9337,10.138,10.138,10.9337,9.15646,10.9337L4.41721,10.9337C3.43569,10.9337,2.64,10.138,2.64,9.15646L2.64,4.41721z M14.8435,4.29873C14.7781,4.29873,14.7251,4.35178,14.7251,4.41721L14.7251,9.15646C14.7251,9.22189,14.7781,9.27494,14.8435,9.27494L19.5828,9.27494C19.6482,9.27494,19.7013,9.22189,19.7013,9.15646L19.7013,4.41721C19.7013,4.35178,19.6482,4.29873,19.5828,4.29873L14.8435,4.29873z M13.0663,4.41721C13.0663,3.43569,13.862,2.64,14.8435,2.64L19.5828,2.64C20.5643,2.64,21.36,3.43569,21.36,4.41721L21.36,9.15646C21.36,10.138,20.5643,10.9337,19.5828,10.9337L14.8435,10.9337C13.862,10.9337,13.0663,10.138,13.0663,9.15646L13.0663,4.41721z M14.8435,14.7251C14.7781,14.7251,14.7251,14.7781,14.7251,14.8435L14.7251,19.5828C14.7251,19.6482,14.7781,19.7013,14.8435,19.7013L19.5828,19.7013C19.6482,19.7013,19.7013,19.6482,19.7013,19.5828L19.7013,14.8435C19.7013,14.7781,19.6482,14.7251,19.5828,14.7251L14.8435,14.7251z M13.0663,14.8435C13.0663,13.862,13.862,13.0663,14.8435,13.0663L19.5828,13.0663C20.5643,13.0663,21.36,13.862,21.36,14.8435L21.36,19.5828C21.36,20.5643,20.5643,21.36,19.5828,21.36L14.8435,21.36C13.862,21.36,13.0663,20.5643,13.0663,19.5828L13.0663,14.8435z M4.41721,14.7251C4.35178,14.7251,4.29873,14.7781,4.29873,14.8435L4.29873,19.5828C4.29873,19.6482,4.35178,19.7013,4.41721,19.7013L9.15646,19.7013C9.22189,19.7013,9.27494,19.6482,9.27494,19.5828L9.27494,14.8435C9.27494,14.7781,9.22189,14.7251,9.15646,14.7251L4.41721,14.7251z M2.64,14.8435C2.64,13.862,3.43569,13.0663,4.41721,13.0663L9.15646,13.0663C10.138,13.0663,10.9337,13.862,10.9337,14.8435L10.9337,19.5828C10.9337,20.5643,10.138,21.36,9.15646,21.36L4.41721,21.36C3.43569,21.36,2.64,20.5643,2.64,19.5828L2.64,14.8435z") - }; - - drawingGroup.Children.Add(geometryDrawing); - - // 设置图像源 - drawingImage.Drawing = drawingGroup; - image.Source = drawingImage; - - return image; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"创建图标图像时出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - - // 返回一个空图像 - return new Image(); - } - } - - /// - /// 鼠标按下事件 - /// - private void Panel_MouseDown(object sender, MouseButtonEventArgs e) - { - try - { - // 提供反馈 - _panel.Background = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0)); - LogHelper.WriteLogToFile("启动台按钮鼠标按下"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启动台按钮鼠标按下事件出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 鼠标抬起事件 - /// - private void Panel_MouseUp(object sender, MouseButtonEventArgs e) - { - try - { - // 只有左键点击才显示启动台窗口 - if (e.ChangedButton != MouseButton.Left) - { - return; - } - - // 恢复背景 - _panel.Background = Brushes.Transparent; - LogHelper.WriteLogToFile("启动台按钮鼠标抬起,准备显示启动台窗口"); - - // 获取按钮在屏幕上的位置 - Point buttonPosition = _panel.PointToScreen(new Point(_panel.ActualWidth / 2, 0)); - - // 显示启动台窗口 - _plugin.ShowLauncherWindow(buttonPosition); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启动台按钮鼠标抬起事件出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - } - } - - /// - /// 鼠标离开事件 - /// - private void Panel_MouseLeave(object sender, MouseEventArgs e) - { - try - { - // 恢复背景 - _panel.Background = Brushes.Transparent; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启动台按钮鼠标离开事件出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherModels.cs b/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherModels.cs deleted file mode 100644 index 9b6ac63b..00000000 --- a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherModels.cs +++ /dev/null @@ -1,332 +0,0 @@ -using Microsoft.Win32; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Drawing; -using System.IO; -using System.Runtime.InteropServices; -using System.Windows; -using System.Windows.Interop; -using System.Windows.Media; -using System.Windows.Media.Imaging; - -namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher -{ - /// - /// 启动台按钮位置 - /// - public enum LauncherButtonPosition - { - /// - /// 左侧 - /// - Left, - - /// - /// 右侧 - /// - Right - } - - /// - /// 启动台配置 - /// - public class LauncherConfig - { - /// - /// 启动台按钮位置 - /// - public LauncherButtonPosition ButtonPosition { get; set; } = LauncherButtonPosition.Right; - - /// - /// 启动台应用程序列表 - /// - public List Items { get; set; } = new List(); - } - - /// - /// 启动台应用项 - /// - public class LauncherItem - { - /// - /// 应用程序名称 - /// - public string Name { get; set; } - - /// - /// 应用程序路径 - /// - public string Path { get; set; } - - /// - /// 是否可见 - /// - public bool IsVisible { get; set; } = true; - - /// - /// 在启动台中的位置(0-39) - /// - public int Position { get; set; } = -1; - - /// - /// 是否已固定位置 - /// - public bool IsPositionFixed { get; set; } = false; - - /// - /// 图标缓存 - /// - [JsonIgnore] - private ImageSource _iconCache; - - /// - /// 获取应用程序图标 - /// - [JsonIgnore] - public ImageSource Icon - { - get - { - if (_iconCache != null) - { - return _iconCache; - } - - try - { - if (File.Exists(Path)) - { - // 从文件中获取图标 - Icon icon = System.Drawing.Icon.ExtractAssociatedIcon(Path); - if (icon != null) - { - _iconCache = Imaging.CreateBitmapSourceFromHIcon( - icon.Handle, - Int32Rect.Empty, - BitmapSizeOptions.FromEmptyOptions()); - - icon.Dispose(); - return _iconCache; - } - } - else - { - // 从注册表中获取文件类型关联图标 - string extension = System.IO.Path.GetExtension(Path); - if (!string.IsNullOrEmpty(extension)) - { - string fileType = Registry.ClassesRoot.OpenSubKey(extension)?.GetValue(string.Empty) as string; - if (!string.IsNullOrEmpty(fileType)) - { - string iconPath = Registry.ClassesRoot.OpenSubKey(fileType + "\\DefaultIcon")?.GetValue(string.Empty) as string; - if (!string.IsNullOrEmpty(iconPath)) - { - string[] parts = iconPath.Split(','); - string iconFile = parts[0].Trim('"'); - int iconIndex = parts.Length > 1 ? Convert.ToInt32(parts[1]) : 0; - - if (File.Exists(iconFile)) - { - Icon icon = IconExtractor.Extract(iconFile, iconIndex, true); - if (icon != null) - { - _iconCache = Imaging.CreateBitmapSourceFromHIcon( - icon.Handle, - Int32Rect.Empty, - BitmapSizeOptions.FromEmptyOptions()); - - icon.Dispose(); - return _iconCache; - } - } - } - } - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"获取应用图标时出错: {ex.Message}", LogHelper.LogType.Error); - } - - // 返回默认图标 - return GetDefaultIcon(); - } - } - - /// - /// 获取默认图标 - /// - private ImageSource GetDefaultIcon() - { - try - { - // 对于资源管理器,使用特定图标 - if (Path.EndsWith("explorer.exe", StringComparison.OrdinalIgnoreCase)) - { - try - { - // 直接从C:\Windows\explorer.exe获取图标 - string explorerPath = @"C:\Windows\explorer.exe"; - if (File.Exists(explorerPath)) - { - Icon icon = System.Drawing.Icon.ExtractAssociatedIcon(explorerPath); - if (icon != null) - { - _iconCache = Imaging.CreateBitmapSourceFromHIcon( - icon.Handle, - Int32Rect.Empty, - BitmapSizeOptions.FromEmptyOptions()); - - icon.Dispose(); - return _iconCache; - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"获取资源管理器图标时出错: {ex.Message}", LogHelper.LogType.Warning); - // 如果获取Windows图标失败,回退到默认图标 - } - - // 回退到备用图标 - string explorerIconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-Fluent", "ic_fluent_folder_24_regular.png"); - if (File.Exists(explorerIconPath)) - { - Uri uri = new Uri(explorerIconPath); - BitmapImage image = new BitmapImage(uri); - _iconCache = image; - return _iconCache; - } - } - - // 返回一个简单的默认图标 - string iconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-png", "icc.png"); - if (File.Exists(iconPath)) - { - Uri uri = new Uri(iconPath); - BitmapImage image = new BitmapImage(uri); - _iconCache = image; - return _iconCache; - } - - // 如果还是没有找到,尝试使用应用程序图标 - string appIconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-Fluent", "ic_fluent_apps_24_regular.png"); - if (File.Exists(appIconPath)) - { - Uri uri = new Uri(appIconPath); - BitmapImage image = new BitmapImage(uri); - _iconCache = image; - return _iconCache; - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"获取默认图标时出错: {ex.Message}", LogHelper.LogType.Error); - } - - return null; - } - - /// - /// 启动应用程序 - /// - public void Launch() - { - try - { - if (string.IsNullOrEmpty(Path)) - { - LogHelper.WriteLogToFile("无法启动应用程序:路径为空", LogHelper.LogType.Error); - return; - } - - // 检查文件是否存在 - if (!File.Exists(Path) && !Path.Contains(":\\")) - { - // 可能是系统命令,如explorer.exe - ProcessStartInfo psi = new ProcessStartInfo - { - FileName = Path, - UseShellExecute = true - }; - Process.Start(psi); - } - else - { - // 使用Process.Start启动应用程序 - ProcessStartInfo psi = new ProcessStartInfo - { - FileName = Path, - UseShellExecute = true - }; - Process.Start(psi); - } - - LogHelper.WriteLogToFile($"已启动应用程序: {Path}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启动应用程序时出错: {ex.Message}", LogHelper.LogType.Error); - MessageBox.Show($"启动应用程序时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); - } - } - } - - /// - /// 图标提取工具类 - /// - public static class IconExtractor - { - /// - /// 从文件中提取图标 - /// - /// 文件路径 - /// 图标索引 - /// 是否提取大图标 - /// 提取的图标 - public static Icon Extract(string file, int index, bool largeIcon) - { - try - { - IntPtr large; - IntPtr small; - ExtractIconEx(file, index, out large, out small, 1); - - try - { - return Icon.FromHandle(largeIcon ? large : small); - } - catch - { - return null; - } - finally - { - if (large != IntPtr.Zero) - DestroyIcon(large); - - if (small != IntPtr.Zero) - DestroyIcon(small); - } - } - catch - { - return null; - } - } - - [DllImport("Shell32.dll", EntryPoint = "ExtractIconEx")] - private static extern int ExtractIconEx( - [MarshalAs(UnmanagedType.LPStr)] string lpszFile, - int nIconIndex, - out IntPtr phiconLarge, - out IntPtr phiconSmall, - int nIcons); - - [DllImport("User32.dll")] - private static extern int DestroyIcon(IntPtr hIcon); - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherSettingsControl.xaml b/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherSettingsControl.xaml deleted file mode 100644 index dde23f42..00000000 --- a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherSettingsControl.xaml +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherWindow.xaml.cs b/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherWindow.xaml.cs deleted file mode 100644 index 6ca80e90..00000000 --- a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherWindow.xaml.cs +++ /dev/null @@ -1,466 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Threading; - -namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher -{ - /// - /// LauncherWindow.xaml 的交互逻辑 - /// - public partial class LauncherWindow : Window - { - /// - /// 父插件 - /// - private readonly SuperLauncherPlugin _plugin; - - /// - /// 是否处于固定模式 - /// - private bool _isFixMode; - - /// - /// 应用项按钮列表 - /// - private readonly Dictionary _appButtons = new Dictionary(); - - /// - /// 拖拽中的按钮 - /// - private Button _draggingButton; - - /// - /// 拖拽开始位置 - /// - private Point _dragStartPoint; - - /// - /// 构造函数 - /// - public LauncherWindow(SuperLauncherPlugin plugin) - { - InitializeComponent(); - - _plugin = plugin; - - // 加载应用项 - LoadLauncherItems(); - - // 添加鼠标按下事件(用于拖动窗口) - MouseDown += (s, e) => - { - if (e.ChangedButton == MouseButton.Left && e.ButtonState == MouseButtonState.Pressed) - { - DragMove(); - } - }; - - // 根据应用数量调整窗口大小 - AdjustWindowSize(); - } - - /// - /// 加载启动台应用项 - /// - private void LoadLauncherItems() - { - // 清空现有应用项 - AppPanel.Children.Clear(); - _appButtons.Clear(); - - // 获取显示的应用项 - var visibleItems = _plugin.LauncherItems - .Where(item => item.IsVisible) - .OrderBy(item => item.Position) - .ToList(); - - foreach (var item in visibleItems) - { - // 创建应用按钮 - Button appButton = new Button - { - Style = (Style)FindResource("LauncherItemStyle"), - DataContext = item, - Tag = item.Position - }; - - // 添加点击事件 - appButton.Click += AppButton_Click; - - // 在固定模式下,添加拖拽事件 - appButton.PreviewMouseDown += AppButton_PreviewMouseDown; - appButton.PreviewMouseMove += AppButton_PreviewMouseMove; - appButton.PreviewMouseUp += AppButton_PreviewMouseUp; - - // 记录按钮和项目的对应关系 - _appButtons.Add(appButton, item); - - // 添加到面板 - AppPanel.Children.Add(appButton); - } - } - - /// - /// 根据应用数量调整窗口大小 - /// - private void AdjustWindowSize() - { - try - { - // 每行最多显示4个应用 - const int appsPerRow = 4; - - // 计算行数 - int visibleCount = _appButtons.Count; - int rowCount = (int)Math.Ceiling(visibleCount / (double)appsPerRow); - - // 设置窗口宽度(每个应用90像素宽 = 80 + 5*2) - Width = Math.Min(appsPerRow * 90 + 40, 400); // 最大宽度400 - - // 设置窗口高度(每个应用90像素高 = 80 + 5*2) - Height = Math.Min(rowCount * 90 + 60, 600); // 最大高度600,标题栏40 + 边距20 - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"调整启动台窗口大小时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 应用按钮点击事件 - /// - private void AppButton_Click(object sender, RoutedEventArgs e) - { - try - { - if (_isFixMode) return; // 在固定模式下,不响应点击事件 - - if (sender is Button button && _appButtons.TryGetValue(button, out LauncherItem item)) - { - // 获取应用路径和名称,用于后续启动 - string appPath = item.Path; - string appName = item.Name; - - LogHelper.WriteLogToFile($"点击启动应用: {appName}, 路径: {appPath}"); - - // 首先标记窗口正在关闭 - IsClosing = true; - - // 创建一个应用启动任务 - var launchTask = new Task(() => - { - try - { - // 等待一段时间,确保窗口关闭流程已经开始 - Thread.Sleep(200); - - // 使用UI线程启动应用 - Application.Current.Dispatcher.Invoke(() => - { - try - { - // 检查应用路径是否存在 - if (File.Exists(appPath) || !appPath.Contains(":\\")) - { - // 创建进程启动信息 - var psi = new ProcessStartInfo - { - FileName = appPath, - UseShellExecute = true, - }; - - // 启动应用程序 - var process = Process.Start(psi); - LogHelper.WriteLogToFile($"应用程序 {appName} 已启动"); - } - else - { - LogHelper.WriteLogToFile($"应用路径不存在: {appPath}", LogHelper.LogType.Error); - MessageBox.Show($"找不到应用程序: {appPath}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启动应用程序失败: {ex.Message}", LogHelper.LogType.Error); - MessageBox.Show($"启动应用程序失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); - } - }); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"应用启动任务出错: {ex.Message}", LogHelper.LogType.Error); - } - }); - - // 关闭窗口 - try - { - Dispatcher.BeginInvoke(new Action(() => - { - try { Close(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } - - // 启动应用程序任务 - launchTask.Start(); - }), DispatcherPriority.Background); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"关闭窗口或启动任务时出错: {ex.Message}", LogHelper.LogType.Error); - // 如果无法通过UI关闭窗口,直接启动任务 - launchTask.Start(); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"应用按钮点击事件出错: {ex.Message}", LogHelper.LogType.Error); - try { IsClosing = true; Close(); } catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); } - } - } - - #region 固定模式拖拽事件 - - /// - /// 应用按钮鼠标按下事件 - /// - private void AppButton_PreviewMouseDown(object sender, MouseButtonEventArgs e) - { - if (!_isFixMode) return; - - if (e.ChangedButton == MouseButton.Left && sender is Button button) - { - _draggingButton = button; - _dragStartPoint = e.GetPosition(AppPanel); - button.CaptureMouse(); - button.Opacity = 0.7; - - // 阻止事件冒泡,以避免触发按钮点击 - e.Handled = true; - } - } - - /// - /// 应用按钮鼠标移动事件 - /// - private void AppButton_PreviewMouseMove(object sender, MouseEventArgs e) - { - if (!_isFixMode || _draggingButton == null) return; - - if (e.LeftButton == MouseButtonState.Pressed) - { - Point currentPosition = e.GetPosition(AppPanel); - - // 移动按钮 - System.Windows.Controls.Canvas.SetLeft(_draggingButton, currentPosition.X - _draggingButton.ActualWidth / 2); - System.Windows.Controls.Canvas.SetTop(_draggingButton, currentPosition.Y - _draggingButton.ActualHeight / 2); - - // 将按钮移到最上层 - Panel.SetZIndex(_draggingButton, 100); - - // 阻止事件冒泡 - e.Handled = true; - } - } - - /// - /// 应用按钮鼠标释放事件 - /// - private void AppButton_PreviewMouseUp(object sender, MouseButtonEventArgs e) - { - if (!_isFixMode || _draggingButton == null) return; - - // 释放鼠标捕获 - _draggingButton.ReleaseMouseCapture(); - - // 计算新位置 - Point releasePoint = e.GetPosition(AppPanel); - int newPosition = CalculateGridPosition(releasePoint); - - // 获取当前项目 - LauncherItem currentItem = _appButtons[_draggingButton]; - - // 重新排序 - ReorderItems(currentItem, newPosition); - - // 重新加载应用项 - LoadLauncherItems(); - - // 保存配置 - _plugin.SaveConfig(); - - // 清除拖拽状态 - _draggingButton.Opacity = 1; - Panel.SetZIndex(_draggingButton, 0); - _draggingButton = null; - - // 阻止事件冒泡 - e.Handled = true; - } - - /// - /// 计算网格位置 - /// - private int CalculateGridPosition(Point point) - { - // 计算行和列 - int columnCount = 4; // 每行最多4个应用 - int columnWidth = 90; // 应用宽度(包括边距) - int rowHeight = 90; // 应用高度(包括边距) - - int column = (int)(point.X / columnWidth); - int row = (int)(point.Y / rowHeight); - - // 确保在有效范围内 - column = Math.Max(0, Math.Min(column, columnCount - 1)); - row = Math.Max(0, row); - - // 计算位置索引 - return row * columnCount + column; - } - - /// - /// 重新排序应用项 - /// - private void ReorderItems(LauncherItem item, int newPosition) - { - try - { - // 设置项目为固定位置 - item.IsPositionFixed = true; - - // 如果位置相同,无需调整 - if (item.Position == newPosition) - { - return; - } - - // 获取所有可见项目 - var visibleItems = _plugin.LauncherItems - .Where(i => i.IsVisible) - .OrderBy(i => i.Position) - .ToList(); - - // 移除当前项目 - visibleItems.Remove(item); - - // 查找插入位置 - int insertIndex = 0; - for (int i = 0; i < visibleItems.Count; i++) - { - if (visibleItems[i].Position >= newPosition) - { - insertIndex = i; - break; - } - insertIndex = i + 1; - } - - // 插入项目 - visibleItems.Insert(insertIndex, item); - - // 重新分配位置 - for (int i = 0; i < visibleItems.Count; i++) - { - visibleItems[i].Position = i; - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"重新排序应用项时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - - #region 窗口事件处理 - - /// - /// 窗口失去焦点事件 - /// - private void Window_Deactivated(object sender, EventArgs e) - { - try - { - // 只有在非固定模式、窗口已加载、未处于关闭状态且IsLoaded=true时关闭窗口 - if (!_isFixMode && IsLoaded && !IsClosing) - { - // 标记为正在关闭 - IsClosing = true; - - // 使用Dispatcher.BeginInvoke而不是直接调用Close,避免冲突 - Dispatcher.BeginInvoke(new Action(() => - { - try - { - // 再次检查窗口状态 - if (IsLoaded && !IsClosing) - { - Close(); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"延迟关闭窗口时出错: {ex.Message}", LogHelper.LogType.Error); - } - }), DispatcherPriority.Background); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"窗口失去焦点关闭时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 窗口是否正在关闭 - /// - private bool IsClosing { get; set; } - - /// - /// 重写OnClosing方法,标记窗口正在关闭 - /// - protected override void OnClosing(CancelEventArgs e) - { - IsClosing = true; - base.OnClosing(e); - } - - /// - /// 关闭按钮点击事件 - /// - private void BtnClose_Click(object sender, RoutedEventArgs e) - { - Close(); - } - - /// - /// 固定模式按钮点击事件 - /// - private void BtnFixMode_Click(object sender, RoutedEventArgs e) - { - // 切换固定模式 - _isFixMode = !_isFixMode; - - // 更新固定模式按钮图标颜色 - FixModeIcon.Fill = _isFixMode ? Brushes.Yellow : Brushes.White; - - // 显示提示 - if (_isFixMode) - { - MessageBox.Show("已进入固定模式,您可以拖动应用图标调整位置。", "提示", MessageBoxButton.OK, MessageBoxImage.Information); - } - } - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncherPlugin.cs b/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncherPlugin.cs deleted file mode 100644 index 555c7bf9..00000000 --- a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncherPlugin.cs +++ /dev/null @@ -1,589 +0,0 @@ -using Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; - -namespace Ink_Canvas.Helpers.Plugins.BuiltIn -{ - /// - /// 超级启动台插件 - /// - public class SuperLauncherPlugin : PluginBase - { - #region 插件基本信息 - - public override string Name => "超级启动台"; - - public override string Description => "在浮动栏添加一个启动台按钮,可快速启动常用应用程序。"; - - public override Version Version => new Version(1, 0, 1); - - public override string Author => "ICC CE 团队"; - - public override bool IsBuiltIn => true; - - #endregion - - #region 插件属性和字段 - - /// - /// 启动台配置 - /// - public LauncherConfig Config { get; private set; } - - /// - /// 启动台应用程序列表 - /// - public ObservableCollection LauncherItems { get; private set; } - - /// - /// 启动台按钮 - /// - private LauncherButton _launcherButton; - - /// - /// 启动台窗口 - /// - private LauncherWindow _launcherWindow; - - /// - /// 配置文件路径 - /// - private readonly string _configPath = Path.Combine(App.RootPath, "PluginConfigs", "SuperLauncher.json"); - - /// - /// 标记是否已添加到浮动栏 - /// - private bool _isAddedToFloatingBar; - - #endregion - - #region 插件生命周期 - - public override void Initialize() - { - try - { - base.Initialize(); - - // 创建配置目录 - string configDir = Path.Combine(App.RootPath, "PluginConfigs"); - if (!Directory.Exists(configDir)) - { - Directory.CreateDirectory(configDir); - } - - // 加载配置 - LoadConfig(); - - LogHelper.WriteLogToFile("超级启动台插件已初始化"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"初始化超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - } - } - - public override void Enable() - { - try - { - if (IsEnabled) return; // 防止重复启用 - - // 创建启动台按钮 - if (_launcherButton == null) - { - _launcherButton = new LauncherButton(this); - LogHelper.WriteLogToFile("超级启动台按钮已创建"); - } - - // 添加启动台按钮到浮动栏 - AddLauncherButtonToFloatingBar(); - - // 设置启用状态 - base.Enable(); - - // 保存插件配置 - SavePluginSettings(); - - LogHelper.WriteLogToFile("超级启动台插件已启用"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启用超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - } - } - - public override void Disable() - { - try - { - if (!IsEnabled) return; // 防止重复禁用 - - // 从浮动栏移除启动台按钮 - RemoveLauncherButtonFromFloatingBar(); - - // 如果启动台窗口打开,则关闭 - if (_launcherWindow != null && _launcherWindow.IsVisible) - { - _launcherWindow.Close(); - _launcherWindow = null; - } - - // 设置禁用状态 - base.Disable(); - - // 保存插件配置 - SavePluginSettings(); - - LogHelper.WriteLogToFile("超级启动台插件已禁用"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"禁用超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - } - } - - public override UserControl GetSettingsView() - { - return new LauncherSettingsControl(this); - } - - public override void Cleanup() - { - // 保存配置 - SaveConfig(); - - // 从浮动栏移除启动台按钮 - RemoveLauncherButtonFromFloatingBar(); - - // 如果启动台窗口打开,则关闭 - if (_launcherWindow != null && _launcherWindow.IsVisible) - { - _launcherWindow.Close(); - _launcherWindow = null; - } - - base.Cleanup(); - } - - /// - /// 保存插件设置 - /// - public override void SavePluginSettings() - { - try - { - // 确保配置已加载 - if (Config == null) - { - LoadConfig(); - } - - // 更新其他设置,但不更改插件启用状态 - - // 保存配置 - SaveConfig(); - - LogHelper.WriteLogToFile("超级启动台插件设置已保存"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存超级启动台插件设置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - - #region 配置管理 - - /// - /// 加载配置 - /// - private void LoadConfig() - { - try - { - if (File.Exists(_configPath)) - { - string json = File.ReadAllText(_configPath); - Config = JsonConvert.DeserializeObject(json) ?? CreateDefaultConfig(); - LauncherItems = new ObservableCollection(Config.Items ?? new List()); - - // 注意:不再根据配置更改插件启用状态 - // 插件状态由PluginManager统一管理 - } - else - { - Config = CreateDefaultConfig(); - LauncherItems = new ObservableCollection(Config.Items); - SaveConfig(); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载超级启动台配置时出错: {ex.Message}", LogHelper.LogType.Error); - Config = CreateDefaultConfig(); - LauncherItems = new ObservableCollection(Config.Items); - } - } - - /// - /// 保存配置 - /// - public void SaveConfig() - { - try - { - // 同步LauncherItems到Config - Config.Items = new List(LauncherItems); - - // 序列化并保存配置 - string json = JsonConvert.SerializeObject(Config, Formatting.Indented); - File.WriteAllText(_configPath, json); - - LogHelper.WriteLogToFile("超级启动台配置已保存"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存超级启动台配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 创建默认配置 - /// - private LauncherConfig CreateDefaultConfig() - { - var config = new LauncherConfig - { - ButtonPosition = LauncherButtonPosition.Right, - // 不再使用IsEnabled,插件状态由PluginManager管理 - Items = new List - { - new LauncherItem - { - Name = "资源管理器", - Path = @"C:\Windows\explorer.exe", - IsVisible = true, - Position = 0 - } - } - }; - - return config; - } - - #endregion - - #region 启动台按钮管理 - - /// - /// 将启动台按钮添加到浮动栏 - /// - private void AddLauncherButtonToFloatingBar() - { - try - { - // 如果已经添加,先移除 - if (_isAddedToFloatingBar) - { - RemoveLauncherButtonFromFloatingBar(); - _isAddedToFloatingBar = false; - } - - // 获取主窗口实例 - var mainWindow = Application.Current.MainWindow; - if (mainWindow == null) - { - LogHelper.WriteLogToFile("未找到主窗口实例,无法添加启动台按钮", LogHelper.LogType.Error); - return; - } - - // 创建启动台按钮 - _launcherButton = new LauncherButton(this); - var buttonElement = _launcherButton.Element; - - // 查找浮动栏 - var floatingBar = mainWindow.FindName("StackPanelFloatingBar") as Panel; - if (floatingBar == null) - { - // 如果直接查找失败,则尝试遍历可视树查找 - Panel floatingBarPanelFromTree = null; - FindStackPanelFloatingBar(mainWindow, ref floatingBarPanelFromTree); - floatingBar = floatingBarPanelFromTree; - } - - if (floatingBar == null) - { - LogHelper.WriteLogToFile("未找到浮动栏,无法添加启动台按钮", LogHelper.LogType.Error); - return; - } - - // 添加启动台按钮到浮动栏 - if (Config.ButtonPosition == LauncherButtonPosition.Left) - { - floatingBar.Children.Insert(0, buttonElement); - LogHelper.WriteLogToFile("启动台按钮已添加到浮动栏左侧"); - } - else - { - floatingBar.Children.Add(buttonElement); - LogHelper.WriteLogToFile("启动台按钮已添加到浮动栏右侧"); - } - - _isAddedToFloatingBar = true; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"添加启动台按钮到浮动栏时出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - } - } - - /// - /// 递归查找StackPanelFloatingBar - /// - private void FindStackPanelFloatingBar(DependencyObject parent, ref Panel result) - { - if (parent == null || result != null) return; - - try - { - // 检查当前对象是否为我们要找的面板 - if (parent is Panel panel && panel.Name == "StackPanelFloatingBar") - { - result = panel; - return; - } - - // 获取子元素数量 - int childCount = VisualTreeHelper.GetChildrenCount(parent); - - // 遍历所有子元素 - for (int i = 0; i < childCount; i++) - { - DependencyObject child = VisualTreeHelper.GetChild(parent, i); - FindStackPanelFloatingBar(child, ref result); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"查找StackPanelFloatingBar时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 从浮动栏移除启动台按钮 - /// - private void RemoveLauncherButtonFromFloatingBar() - { - try - { - if (!_isAddedToFloatingBar || _launcherButton == null) - { - return; - } - - // 获取主窗口实例 - var mainWindow = Application.Current.MainWindow; - if (mainWindow == null) - { - LogHelper.WriteLogToFile("未找到主窗口实例,无法移除启动台按钮", LogHelper.LogType.Error); - return; - } - - // 获取按钮元素 - var buttonElement = _launcherButton.Element; - - // 查找浮动栏 - var floatingBar = mainWindow.FindName("StackPanelFloatingBar") as Panel; - if (floatingBar == null) - { - // 如果直接查找失败,则尝试遍历可视树查找 - Panel floatingBarPanelFromTree = null; - FindStackPanelFloatingBar(mainWindow, ref floatingBarPanelFromTree); - floatingBar = floatingBarPanelFromTree; - } - - if (floatingBar == null) - { - LogHelper.WriteLogToFile("未找到浮动栏,无法移除启动台按钮", LogHelper.LogType.Error); - return; - } - - // 从浮动栏移除启动台按钮 - if (floatingBar.Children.Contains(buttonElement)) - { - floatingBar.Children.Remove(buttonElement); - LogHelper.WriteLogToFile("启动台按钮已从浮动栏移除"); - } - - _isAddedToFloatingBar = false; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"移除启动台按钮时出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - } - } - - /// - /// 更新启动台按钮位置 - /// - public void UpdateButtonPosition() - { - try - { - // 如果按钮已添加到浮动栏,重新添加以更新位置 - if (_isAddedToFloatingBar) - { - RemoveLauncherButtonFromFloatingBar(); - AddLauncherButtonToFloatingBar(); - LogHelper.WriteLogToFile($"启动台按钮位置已更新为: {Config.ButtonPosition}"); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"更新启动台按钮位置时出错: {ex.Message}", LogHelper.LogType.Error); - LogHelper.NewLog(ex); - } - } - - #endregion - - #region 启动台功能 - - /// - /// 显示启动台窗口 - /// - /// 按钮在屏幕上的位置 - public void ShowLauncherWindow(Point buttonPosition) - { - try - { - // 如果窗口已存在,关闭它 - if (_launcherWindow != null && _launcherWindow.IsVisible) - { - _launcherWindow.Close(); - _launcherWindow = null; - return; - } - - // 创建新的启动台窗口 - _launcherWindow = new LauncherWindow(this); - - // 计算窗口位置,使其位于按钮上方 - PositionLauncherWindow(_launcherWindow, buttonPosition); - - // 显示窗口 - _launcherWindow.Show(); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"显示启动台窗口时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 设置启动台窗口位置 - /// - /// 启动台窗口 - /// 按钮在屏幕上的位置 - private void PositionLauncherWindow(LauncherWindow window, Point buttonPosition) - { - // 确保窗口已加载 - if (window.ActualWidth == 0 || window.ActualHeight == 0) - { - window.WindowStartupLocation = WindowStartupLocation.CenterScreen; - - // 设置窗口加载完成后的位置 - window.Loaded += (s, e) => - { - // 窗口位于按钮上方居中 - double left = buttonPosition.X - (window.ActualWidth / 2); - double top = buttonPosition.Y - window.ActualHeight - 10; // 在按钮上方留出一些间距 - - // 确保窗口在屏幕内 - left = Math.Max(0, Math.Min(left, SystemParameters.WorkArea.Width - window.ActualWidth)); - top = Math.Max(0, Math.Min(top, SystemParameters.WorkArea.Height - window.ActualHeight)); - - window.Left = left; - window.Top = top; - }; - } - else - { - // 窗口位于按钮上方居中 - double left = buttonPosition.X - (window.ActualWidth / 2); - double top = buttonPosition.Y - window.ActualHeight - 10; // 在按钮上方留出一些间距 - - // 确保窗口在屏幕内 - left = Math.Max(0, Math.Min(left, SystemParameters.WorkArea.Width - window.ActualWidth)); - top = Math.Max(0, Math.Min(top, SystemParameters.WorkArea.Height - window.ActualHeight)); - - window.Left = left; - window.Top = top; - } - } - - /// - /// 添加应用到启动台 - /// - /// 启动台项 - public void AddLauncherItem(LauncherItem item) - { - // 如果项目数量已达上限,则不添加 - if (LauncherItems.Count >= 40) - { - MessageBox.Show("启动台项目数量已达上限(40个)!", "提示", MessageBoxButton.OK, MessageBoxImage.Information); - return; - } - - // 寻找合适的位置 - if (item.Position < 0) - { - item.Position = FindNextAvailablePosition(); - } - - // 添加项目并保存配置 - LauncherItems.Add(item); - SaveConfig(); - } - - /// - /// 查找下一个可用位置 - /// - private int FindNextAvailablePosition() - { - // 获取已使用的位置列表 - var usedPositions = new HashSet(); - foreach (var item in LauncherItems) - { - usedPositions.Add(item.Position); - } - - // 查找第一个可用位置 - for (int i = 0; i < 40; i++) - { - if (!usedPositions.Contains(i)) - { - return i; - } - } - - // 如果所有位置都已使用,则返回0 - return 0; - } - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/EnhancedPluginBase.cs b/Ink Canvas/Helpers/Plugins/EnhancedPluginBase.cs deleted file mode 100644 index d22ca80e..00000000 --- a/Ink Canvas/Helpers/Plugins/EnhancedPluginBase.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Windows.Controls; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 增强的插件基类,提供对插件服务的访问和基本实现 - /// - public abstract class EnhancedPluginBase : PluginBase, IEnhancedPlugin - { - /// - /// 插件服务实例 - /// - public IPluginService PluginService { get; private set; } - - /// - /// 构造函数 - /// - protected EnhancedPluginBase() - { - PluginService = PluginServiceManager.Instance; - } - - /// - /// 插件启动时调用,在Initialize之后 - /// - public virtual void OnStartup() - { - LogHelper.WriteLogToFile($"插件 {Name} 已启动"); - } - - /// - /// 插件关闭时调用,在Cleanup之前 - /// - public virtual void OnShutdown() - { - LogHelper.WriteLogToFile($"插件 {Name} 正在关闭"); - } - - /// - /// 获取插件的菜单项 - /// - /// 菜单项集合 - public virtual MenuItem[] GetMenuItems() - { - return new MenuItem[0]; - } - - /// - /// 获取插件的工具栏按钮 - /// - /// 工具栏按钮集合 - public virtual Button[] GetToolbarButtons() - { - return new Button[0]; - } - - /// - /// 获取插件的状态栏信息 - /// - /// 状态栏信息 - public virtual string GetStatusBarInfo() - { - return $"{Name} v{Version} - {(IsEnabled ? "已启用" : "已禁用")}"; - } - - /// - /// 插件配置变更时调用 - /// - public virtual void OnConfigurationChanged() - { - LogHelper.WriteLogToFile($"插件 {Name} 配置已变更"); - } - - /// - /// 重写初始化方法,调用OnStartup - /// - public override void Initialize() - { - base.Initialize(); - OnStartup(); - } - - /// - /// 重写清理方法,调用OnShutdown - /// - public override void Cleanup() - { - OnShutdown(); - base.Cleanup(); - } - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/EnhancedPluginBaseV2.cs b/Ink Canvas/Helpers/Plugins/EnhancedPluginBaseV2.cs deleted file mode 100644 index 218068b6..00000000 --- a/Ink Canvas/Helpers/Plugins/EnhancedPluginBaseV2.cs +++ /dev/null @@ -1,241 +0,0 @@ -using System.Windows.Controls; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 增强的插件基类 V2,提供对三个专门服务接口的访问 - /// 插件开发者可以根据需要选择性地使用这些服务 - /// - public abstract class EnhancedPluginBaseV2 : PluginBase, IEnhancedPlugin - { - /// - /// 获取服务实例 - /// - public IGetService GetService { get; private set; } - - /// - /// 窗口服务实例 - /// - public IWindowService WindowService { get; private set; } - - /// - /// 操作服务实例 - /// - public IActionService ActionService { get; private set; } - - /// - /// 插件服务实例(兼容性) - /// - public IPluginService PluginService { get; private set; } - - /// - /// 构造函数 - /// - protected EnhancedPluginBaseV2() - { - // 初始化所有服务实例 - PluginService = PluginServiceManager.Instance; - GetService = PluginServiceManager.Instance; - WindowService = PluginServiceManager.Instance; - ActionService = PluginServiceManager.Instance; - } - - /// - /// 插件启动时调用,在Initialize之后 - /// - public virtual void OnStartup() - { - LogHelper.WriteLogToFile($"插件 {Name} 已启动"); - } - - /// - /// 插件关闭时调用,在Cleanup之前 - /// - public virtual void OnShutdown() - { - LogHelper.WriteLogToFile($"插件 {Name} 正在关闭"); - } - - /// - /// 获取插件的菜单项 - /// - /// 菜单项集合 - public virtual MenuItem[] GetMenuItems() - { - return new MenuItem[0]; - } - - /// - /// 获取插件的工具栏按钮 - /// - /// 工具栏按钮集合 - public virtual Button[] GetToolbarButtons() - { - return new Button[0]; - } - - /// - /// 获取插件的状态栏信息 - /// - /// 状态栏信息 - public virtual string GetStatusBarInfo() - { - return $"{Name} v{Version} - {(IsEnabled ? "已启用" : "已禁用")}"; - } - - /// - /// 插件配置变更时调用 - /// - public virtual void OnConfigurationChanged() - { - LogHelper.WriteLogToFile($"插件 {Name} 配置已变更"); - } - - #region 便捷方法 - - /// - /// 显示通知消息 - /// - /// 消息内容 - /// 消息类型 - protected void ShowNotification(string message, NotificationType type = NotificationType.Info) - { - WindowService.ShowNotification(message, type); - } - - /// - /// 显示确认对话框 - /// - /// 消息内容 - /// 标题 - /// 用户选择结果 - protected bool ShowConfirmDialog(string message, string title = "确认") - { - return WindowService.ShowConfirmDialog(message, title); - } - - /// - /// 显示输入对话框 - /// - /// 提示消息 - /// 标题 - /// 默认值 - /// 用户输入内容 - protected string ShowInputDialog(string message, string title = "输入", string defaultValue = "") - { - return WindowService.ShowInputDialog(message, title, defaultValue); - } - - /// - /// 获取系统设置 - /// - /// 设置类型 - /// 设置键 - /// 默认值 - /// 设置值 - protected T GetSetting(string key, T defaultValue = default(T)) - { - return GetService.GetSetting(key, defaultValue); - } - - /// - /// 设置系统设置 - /// - /// 设置类型 - /// 设置键 - /// 设置值 - protected void SetSetting(string key, T value) - { - ActionService.SetSetting(key, value); - } - - /// - /// 保存设置 - /// - protected void SaveSettings() - { - ActionService.SaveSettings(); - } - - /// - /// 清除当前画布 - /// - protected void ClearCanvas() - { - ActionService.ClearCanvas(); - } - - /// - /// 撤销操作 - /// - protected void Undo() - { - ActionService.Undo(); - } - - /// - /// 重做操作 - /// - protected void Redo() - { - ActionService.Redo(); - } - - /// - /// 检查是否可以撤销 - /// - protected bool CanUndo => GetService.CanUndo; - - /// - /// 检查是否可以重做 - /// - protected bool CanRedo => GetService.CanRedo; - - /// - /// 获取当前绘制模式 - /// - protected int CurrentDrawingMode => GetService.CurrentDrawingMode; - - /// - /// 设置绘制模式 - /// - /// 绘制模式 - protected void SetDrawingMode(int mode) - { - ActionService.SetDrawingMode(mode); - } - - /// - /// 注册事件处理器 - /// - /// 事件名称 - /// 事件处理器 - protected void RegisterEventHandler(string eventName, System.EventHandler handler) - { - ActionService.RegisterEventHandler(eventName, handler); - } - - /// - /// 注销事件处理器 - /// - /// 事件名称 - /// 事件处理器 - protected void UnregisterEventHandler(string eventName, System.EventHandler handler) - { - ActionService.UnregisterEventHandler(eventName, handler); - } - - /// - /// 触发事件 - /// - /// 事件名称 - /// 事件发送者 - /// 事件参数 - protected void TriggerEvent(string eventName, object sender, System.EventArgs args) - { - ActionService.TriggerEvent(eventName, sender, args); - } - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/IActionService.cs b/Ink Canvas/Helpers/Plugins/IActionService.cs deleted file mode 100644 index 99c61473..00000000 --- a/Ink Canvas/Helpers/Plugins/IActionService.cs +++ /dev/null @@ -1,296 +0,0 @@ -using System; -using System.Windows.Media; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 操作服务接口,统一所有执行操作相关的方法 - /// - public interface IActionService - { - #region 画布操作 - - /// - /// 清除当前画布 - /// - void ClearCanvas(); - - /// - /// 清除所有画布 - /// - void ClearAllCanvases(); - - /// - /// 添加新页面 - /// - void AddNewPage(); - - /// - /// 删除当前页面 - /// - void DeleteCurrentPage(); - - /// - /// 切换到指定页面 - /// - /// 页面索引 - void SwitchToPage(int pageIndex); - - /// - /// 切换到下一页 - /// - void NextPage(); - - /// - /// 切换到上一页 - /// - void PreviousPage(); - - #endregion - - #region 绘制操作 - - /// - /// 设置绘制模式 - /// - /// 绘制模式 - void SetDrawingMode(int mode); - - /// - /// 设置笔触宽度 - /// - /// 宽度 - void SetInkWidth(double width); - - /// - /// 设置笔触颜色 - /// - /// 颜色 - void SetInkColor(Color color); - - /// - /// 设置高亮笔宽度 - /// - /// 宽度 - void SetHighlighterWidth(double width); - - /// - /// 设置橡皮擦大小 - /// - /// 大小 - void SetEraserSize(int size); - - /// - /// 设置橡皮擦类型 - /// - /// 类型 - void SetEraserType(int type); - - /// - /// 设置橡皮擦形状 - /// - /// 形状 - void SetEraserShape(int shape); - - /// - /// 设置笔触透明度 - /// - /// 透明度 - void SetInkAlpha(double alpha); - - /// - /// 设置笔触样式 - /// - /// 样式 - void SetInkStyle(int style); - - /// - /// 设置背景颜色 - /// - /// 颜色 - void SetBackgroundColor(string color); - - #endregion - - #region 文件操作 - - /// - /// 保存画布内容 - /// - /// 文件路径 - void SaveCanvas(string filePath); - - /// - /// 加载画布内容 - /// - /// 文件路径 - void LoadCanvas(string filePath); - - /// - /// 导出为图片 - /// - /// 文件路径 - /// 图片格式 - void ExportAsImage(string filePath, string format); - - /// - /// 导出为PDF - /// - /// 文件路径 - void ExportAsPDF(string filePath); - - #endregion - - #region 撤销重做操作 - - /// - /// 撤销操作 - /// - void Undo(); - - /// - /// 重做操作 - /// - void Redo(); - - #endregion - - #region 选择操作 - - /// - /// 全选 - /// - void SelectAll(); - - /// - /// 取消选择 - /// - void DeselectAll(); - - /// - /// 删除选中内容 - /// - void DeleteSelected(); - - /// - /// 复制选中内容 - /// - void CopySelected(); - - /// - /// 剪切选中内容 - /// - void CutSelected(); - - /// - /// 粘贴内容 - /// - void Paste(); - - #endregion - - #region 系统设置操作 - - /// - /// 设置系统设置 - /// - /// 设置类型 - /// 设置键 - /// 设置值 - void SetSetting(string key, T value); - - /// - /// 保存设置到文件 - /// - void SaveSettings(); - - /// - /// 从文件加载设置 - /// - void LoadSettings(); - - /// - /// 重置设置为默认值 - /// - void ResetSettings(); - - #endregion - - #region 插件管理操作 - - /// - /// 启用插件 - /// - /// 插件名称 - void EnablePlugin(string pluginName); - - /// - /// 禁用插件 - /// - /// 插件名称 - void DisablePlugin(string pluginName); - - /// - /// 卸载插件 - /// - /// 插件名称 - void UnloadPlugin(string pluginName); - - #endregion - - #region 事件系统操作 - - /// - /// 注册事件处理器 - /// - /// 事件名称 - /// 事件处理器 - void RegisterEventHandler(string eventName, EventHandler handler); - - /// - /// 注销事件处理器 - /// - /// 事件名称 - /// 事件处理器 - void UnregisterEventHandler(string eventName, EventHandler handler); - - /// - /// 触发事件 - /// - /// 事件名称 - /// 事件发送者 - /// 事件参数 - void TriggerEvent(string eventName, object sender, EventArgs args); - - #endregion - - #region 应用程序操作 - - /// - /// 重启应用程序 - /// - void RestartApplication(); - - /// - /// 退出应用程序 - /// - void ExitApplication(); - - /// - /// 检查更新 - /// - void CheckForUpdates(); - - /// - /// 打开帮助文档 - /// - void OpenHelpDocument(); - - /// - /// 打开关于页面 - /// - void OpenAboutPage(); - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/ICCPPPluginAdapter.cs b/Ink Canvas/Helpers/Plugins/ICCPPPluginAdapter.cs deleted file mode 100644 index e018ac0d..00000000 --- a/Ink Canvas/Helpers/Plugins/ICCPPPluginAdapter.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.IO; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// ICCPP 插件适配器,用于加载和管理 .iccpp 格式的插件 - /// - public class ICCPPPluginAdapter : PluginBase - { - private readonly byte[] _pluginData; - private readonly string _pluginPath; - private readonly string _pluginName; - private readonly Version _pluginVersion; - private bool _isInitialized; - - /// - /// 创建 ICCPP 插件适配器 - /// - /// 插件文件路径 - /// 插件文件数据 - public ICCPPPluginAdapter(string pluginPath, byte[] pluginData) - { - _pluginPath = pluginPath; - _pluginData = pluginData; - PluginPath = pluginPath; - - // 从文件名获取插件名称 - _pluginName = Path.GetFileNameWithoutExtension(pluginPath); - _pluginVersion = new Version(1, 0, 0); // 默认版本 - - // 尝试从插件数据中读取更多信息 - TryReadPluginMetadata(); - } - - public ICCPPPluginAdapter() - { - _pluginPath = string.Empty; - _pluginData = new byte[0]; - PluginPath = string.Empty; - _pluginName = "ICCPPPlugin"; - _pluginVersion = new Version(1, 0, 0); - // 可选:初始化其他字段 - } - - /// - /// 尝试从插件数据中读取元数据 - /// - private void TryReadPluginMetadata() - { - try - { - // 这里可以根据 .iccpp 文件的实际格式解析元数据 - // 例如,如果文件有特定的头部结构,可以在这里解析 - - // 示例:如果前100字节包含元数据 - if (_pluginData.Length > 100) - { - // 解析元数据的代码... - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"解析插件 {_pluginName} 元数据时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #region IPlugin 接口实现 - - /// - /// 插件名称 - /// - public override string Name => _pluginName; - - /// - /// 插件描述 - /// - public override string Description => $"{_pluginName} (ICCPP 格式插件)"; - - /// - /// 插件版本 - /// - public override Version Version => _pluginVersion; - - /// - /// 插件作者 - /// - public override string Author => "未知"; - - /// - /// 是否为内置插件 - /// - public override bool IsBuiltIn => false; - - /// - /// 初始化插件 - /// - public override void Initialize() - { - if (_isInitialized) return; - - try - { - // 这里可以添加 .iccpp 插件的初始化逻辑 - // 例如,根据文件格式加载特定资源 - - LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已初始化"); - _isInitialized = true; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"初始化 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 启用插件 - /// - public override void Enable() - { - if (IsEnabled) return; - - try - { - // 这里可以添加 .iccpp 插件的启用逻辑 - // 例如,加载动态库、注册事件等 - - base.Enable(); // 设置启用状态并触发事件 - LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已启用"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启用 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 禁用插件 - /// - public override void Disable() - { - if (!IsEnabled) return; - - try - { - // 这里可以添加 .iccpp 插件的禁用逻辑 - // 例如,卸载动态库、注销事件等 - - base.Disable(); // 设置禁用状态并触发事件 - LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已禁用"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"禁用 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 清理插件资源 - /// - public override void Cleanup() - { - try - { - // 这里可以添加 .iccpp 插件的清理逻辑 - // 例如,释放资源等 - - LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已清理资源"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"清理 ICCPP 插件 {Name} 资源时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/IEnhancedPlugin.cs b/Ink Canvas/Helpers/Plugins/IEnhancedPlugin.cs deleted file mode 100644 index 312eb10c..00000000 --- a/Ink Canvas/Helpers/Plugins/IEnhancedPlugin.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Windows.Controls; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 增强的插件接口,提供对插件服务的访问 - /// - public interface IEnhancedPlugin : IPlugin - { - /// - /// 获取插件服务实例 - /// - IPluginService PluginService { get; } - - /// - /// 插件启动时调用,在Initialize之后 - /// - void OnStartup(); - - /// - /// 插件关闭时调用,在Cleanup之前 - /// - void OnShutdown(); - - /// - /// 获取插件的菜单项 - /// - /// 菜单项集合 - MenuItem[] GetMenuItems(); - - /// - /// 获取插件的工具栏按钮 - /// - /// 工具栏按钮集合 - Button[] GetToolbarButtons(); - - /// - /// 获取插件的状态栏信息 - /// - /// 状态栏信息 - string GetStatusBarInfo(); - - /// - /// 插件配置变更时调用 - /// - void OnConfigurationChanged(); - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/IGetService.cs b/Ink Canvas/Helpers/Plugins/IGetService.cs deleted file mode 100644 index ab48889a..00000000 --- a/Ink Canvas/Helpers/Plugins/IGetService.cs +++ /dev/null @@ -1,214 +0,0 @@ -using System.Collections.Generic; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 获取服务接口,统一所有获取类的方法 - /// - public interface IGetService - { - #region 窗口和UI获取 - - /// - /// 获取主窗口引用 - /// - Window MainWindow { get; } - - /// - /// 获取当前画布 - /// - InkCanvas CurrentCanvas { get; } - - /// - /// 获取所有画布页面 - /// - List AllCanvasPages { get; } - - /// - /// 获取当前页面索引 - /// - int CurrentPageIndex { get; } - - /// - /// 获取当前页面数量 - /// - int TotalPageCount { get; } - - /// - /// 获取浮动工具栏 - /// - FrameworkElement FloatingToolBar { get; } - - /// - /// 获取左侧面板 - /// - FrameworkElement LeftPanel { get; } - - /// - /// 获取右侧面板 - /// - FrameworkElement RightPanel { get; } - - /// - /// 获取顶部面板 - /// - FrameworkElement TopPanel { get; } - - /// - /// 获取底部面板 - /// - FrameworkElement BottomPanel { get; } - - #endregion - - #region 绘制工具状态获取 - - /// - /// 获取当前绘制模式 - /// - int CurrentDrawingMode { get; } - - /// - /// 获取当前笔触宽度 - /// - double CurrentInkWidth { get; } - - /// - /// 获取当前笔触颜色 - /// - Color CurrentInkColor { get; } - - /// - /// 获取当前高亮笔宽度 - /// - double CurrentHighlighterWidth { get; } - - /// - /// 获取当前橡皮擦大小 - /// - int CurrentEraserSize { get; } - - /// - /// 获取当前橡皮擦类型 - /// - int CurrentEraserType { get; } - - /// - /// 获取当前橡皮擦形状 - /// - int CurrentEraserShape { get; } - - /// - /// 获取当前笔触透明度 - /// - double CurrentInkAlpha { get; } - - /// - /// 获取当前笔触样式 - /// - int CurrentInkStyle { get; } - - /// - /// 获取当前背景颜色 - /// - string CurrentBackgroundColor { get; } - - #endregion - - #region 应用状态获取 - - /// - /// 获取当前主题模式 - /// - bool IsDarkTheme { get; } - - /// - /// 获取当前是否为白板模式 - /// - bool IsWhiteboardMode { get; } - - /// - /// 获取当前是否为PPT模式 - /// - bool IsPPTMode { get; } - - /// - /// 获取当前是否为全屏模式 - /// - bool IsFullScreenMode { get; } - - /// - /// 获取当前是否为画板模式 - /// - bool IsCanvasMode { get; } - - /// - /// 获取当前是否为选择模式 - /// - bool IsSelectionMode { get; } - - /// - /// 获取当前是否为擦除模式 - /// - bool IsEraserMode { get; } - - /// - /// 获取当前是否为形状绘制模式 - /// - bool IsShapeDrawingMode { get; } - - /// - /// 获取当前是否为高亮模式 - /// - bool IsHighlighterMode { get; } - - #endregion - - #region 撤销重做状态获取 - - /// - /// 获取是否可以撤销 - /// - bool CanUndo { get; } - - /// - /// 获取是否可以重做 - /// - bool CanRedo { get; } - - #endregion - - #region 系统设置获取 - - /// - /// 获取系统设置 - /// - /// 设置类型 - /// 设置键 - /// 默认值 - /// 设置值 - T GetSetting(string key, T defaultValue = default(T)); - - #endregion - - #region 插件信息获取 - - /// - /// 获取所有已加载的插件 - /// - /// 插件列表 - List GetAllPlugins(); - - /// - /// 获取指定插件 - /// - /// 插件名称 - /// 插件实例 - IPlugin GetPlugin(string pluginName); - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/IPlugin.cs b/Ink Canvas/Helpers/Plugins/IPlugin.cs deleted file mode 100644 index 0a43ab15..00000000 --- a/Ink Canvas/Helpers/Plugins/IPlugin.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Windows.Controls; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 定义插件的基本接口 - /// - public interface IPlugin - { - /// - /// 插件名称 - /// - string Name { get; } - - /// - /// 插件描述 - /// - string Description { get; } - - /// - /// 插件版本 - /// - Version Version { get; } - - /// - /// 插件作者 - /// - string Author { get; } - - /// - /// 是否为内置插件 - /// - bool IsBuiltIn { get; } - - /// - /// 初始化插件 - /// 此方法在插件加载时被调用,用于执行一些初始化工作 - /// - void Initialize(); - - /// - /// 启用插件 - /// 此方法在插件被用户或系统启用时调用,激活插件功能 - /// - void Enable(); - - /// - /// 禁用插件 - /// 此方法在插件被用户或系统禁用时调用,停用插件功能 - /// - void Disable(); - - /// - /// 获取插件设置界面 - /// 此方法返回插件的设置界面控件,用于展示在设置窗口 - /// - /// 插件设置界面 - UserControl GetSettingsView(); - - /// - /// 插件卸载时的清理工作 - /// 此方法在插件被卸载前调用,用于释放资源和执行清理 - /// - void Cleanup(); - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/IPluginService.cs b/Ink Canvas/Helpers/Plugins/IPluginService.cs deleted file mode 100644 index 9559a992..00000000 --- a/Ink Canvas/Helpers/Plugins/IPluginService.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 插件服务接口,提供对软件内部功能的访问 - /// 继承自三个专门的服务接口:获取服务、窗口服务、操作服务 - /// - public interface IPluginService : IGetService, IWindowService, IActionService - { - // 这个接口现在继承自三个专门的服务接口 - // 所有方法都在子接口中定义,这里不需要重复定义 - } - - /// - /// 通知类型枚举 - /// - public enum NotificationType - { - /// - /// 信息 - /// - Info, - - /// - /// 成功 - /// - Success, - - /// - /// 警告 - /// - Warning, - - /// - /// 错误 - /// - Error - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/IWindowService.cs b/Ink Canvas/Helpers/Plugins/IWindowService.cs deleted file mode 100644 index eb6fb863..00000000 --- a/Ink Canvas/Helpers/Plugins/IWindowService.cs +++ /dev/null @@ -1,152 +0,0 @@ -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 窗口服务接口,统一所有窗口操作相关的方法 - /// - public interface IWindowService - { - #region 窗口显示和隐藏 - - /// - /// 显示设置窗口 - /// - void ShowSettingsWindow(); - - /// - /// 隐藏设置窗口 - /// - void HideSettingsWindow(); - - /// - /// 显示插件设置窗口 - /// - void ShowPluginSettingsWindow(); - - /// - /// 隐藏插件设置窗口 - /// - void HidePluginSettingsWindow(); - - /// - /// 显示帮助窗口 - /// - void ShowHelpWindow(); - - /// - /// 隐藏帮助窗口 - /// - void HideHelpWindow(); - - /// - /// 显示关于窗口 - /// - void ShowAboutWindow(); - - /// - /// 隐藏关于窗口 - /// - void HideAboutWindow(); - - #endregion - - #region 对话框和通知 - - /// - /// 显示通知消息 - /// - /// 消息内容 - /// 消息类型 - void ShowNotification(string message, NotificationType type = NotificationType.Info); - - /// - /// 显示确认对话框 - /// - /// 消息内容 - /// 标题 - /// 用户选择结果 - bool ShowConfirmDialog(string message, string title = "确认"); - - /// - /// 显示输入对话框 - /// - /// 提示消息 - /// 标题 - /// 默认值 - /// 用户输入内容 - string ShowInputDialog(string message, string title = "输入", string defaultValue = ""); - - #endregion - - #region 窗口状态控制 - - /// - /// 设置窗口全屏状态 - /// - /// 是否全屏 - void SetFullScreen(bool isFullScreen); - - /// - /// 设置窗口置顶状态 - /// - /// 是否置顶 - void SetTopMost(bool isTopMost); - - /// - /// 设置窗口可见性 - /// - /// 是否可见 - void SetWindowVisibility(bool isVisible); - - /// - /// 最小化窗口 - /// - void MinimizeWindow(); - - /// - /// 最大化窗口 - /// - void MaximizeWindow(); - - /// - /// 恢复窗口 - /// - void RestoreWindow(); - - /// - /// 关闭窗口 - /// - void CloseWindow(); - - #endregion - - #region 窗口位置和大小 - - /// - /// 设置窗口位置 - /// - /// X坐标 - /// Y坐标 - void SetWindowPosition(double x, double y); - - /// - /// 设置窗口大小 - /// - /// 宽度 - /// 高度 - void SetWindowSize(double width, double height); - - /// - /// 获取窗口位置 - /// - /// 窗口位置 - (double x, double y) GetWindowPosition(); - - /// - /// 获取窗口大小 - /// - /// 窗口大小 - (double width, double height) GetWindowSize(); - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/PluginBase.cs b/Ink Canvas/Helpers/Plugins/PluginBase.cs deleted file mode 100644 index 716b7e61..00000000 --- a/Ink Canvas/Helpers/Plugins/PluginBase.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System; -using System.Windows.Controls; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 插件基类,提供基本实现 - /// - public abstract class PluginBase : IPlugin - { - /// - /// 插件状态(私有字段) - /// - private bool _isEnabled; - - /// - /// 插件状态(公共属性) - /// - public bool IsEnabled - { - get => _isEnabled; - protected set - { - if (_isEnabled != value) - { - _isEnabled = value; - OnEnabledStateChanged(value); - } - } - } - - /// - /// 插件ID - /// - public string Id { get; protected set; } - - /// - /// 插件路径 - /// - public string PluginPath { get; set; } - - /// - /// 插件名称 - /// - public abstract string Name { get; } - - /// - /// 插件描述 - /// - public abstract string Description { get; } - - /// - /// 插件版本 - /// - public abstract Version Version { get; } - - /// - /// 插件作者 - /// - public abstract string Author { get; } - - /// - /// 是否为内置插件 - /// - public virtual bool IsBuiltIn => false; - - /// - /// 状态变更事件 - /// - public event EventHandler EnabledStateChanged; - - /// - /// 初始化插件 - /// - public virtual void Initialize() - { - Id = GetType().FullName; - - // 添加日志,记录插件名称 - try - { - string name = Name; - LogHelper.WriteLogToFile($"初始化插件: ID={Id}, 名称={name ?? "未命名"}"); - - if (string.IsNullOrEmpty(name)) - { - LogHelper.WriteLogToFile($"警告: 插件 {Id} 的名称为空", LogHelper.LogType.Warning); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"获取插件名称时出错: {ex.Message}", LogHelper.LogType.Error); - } - - LogHelper.WriteLogToFile($"插件 {Name} 已初始化"); - } - - /// - /// 启用插件 - /// - public virtual void Enable() - { - if (!IsEnabled) - { - IsEnabled = true; - LogHelper.WriteLogToFile($"插件 {Name} 已启用"); - } - } - - /// - /// 禁用插件 - /// - public virtual void Disable() - { - if (IsEnabled) - { - IsEnabled = false; - LogHelper.WriteLogToFile($"插件 {Name} 已禁用"); - } - } - - /// - /// 获取插件设置界面 - /// - /// 插件设置界面 - public virtual UserControl GetSettingsView() - { - // 默认返回空设置页面 - return new UserControl(); - } - - /// - /// 插件卸载时的清理工作 - /// - public virtual void Cleanup() - { - LogHelper.WriteLogToFile($"插件 {Name} 已卸载"); - } - - /// - /// 保存插件自身的设置 - /// 注意:此方法仅用于保存插件的特定设置,不应影响插件启用/禁用状态 - /// 插件启用状态由PluginManager统一管理 - /// - public virtual void SavePluginSettings() - { - // 默认实现不做任何事情 - // 子类可以重写此方法,将自身设置保存到配置文件中 - LogHelper.WriteLogToFile($"插件 {Name} 设置已保存", LogHelper.LogType.Event); - } - - /// - /// 触发状态变更事件 - /// - /// 是否启用 - protected virtual void OnEnabledStateChanged(bool isEnabled) - { - EnabledStateChanged?.Invoke(this, isEnabled); - } - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/PluginConfigurationManager.cs b/Ink Canvas/Helpers/Plugins/PluginConfigurationManager.cs deleted file mode 100644 index 9688cd31..00000000 --- a/Ink Canvas/Helpers/Plugins/PluginConfigurationManager.cs +++ /dev/null @@ -1,273 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 插件配置管理器,允许插件管理自己的配置 - /// - public class PluginConfigurationManager - { - private static readonly string PluginConfigDirectory = Path.Combine(App.RootPath, "PluginConfigs"); - private static readonly Dictionary> _pluginConfigs = new Dictionary>(); - private static readonly object _lockObject = new object(); - - static PluginConfigurationManager() - { - // 确保配置目录存在 - if (!Directory.Exists(PluginConfigDirectory)) - { - Directory.CreateDirectory(PluginConfigDirectory); - } - } - - /// - /// 获取插件配置值 - /// - /// 配置值类型 - /// 插件名称 - /// 配置键 - /// 默认值 - /// 配置值 - public static T GetConfiguration(string pluginName, string key, T defaultValue = default(T)) - { - lock (_lockObject) - { - try - { - if (_pluginConfigs.TryGetValue(pluginName, out var pluginConfig)) - { - if (pluginConfig.TryGetValue(key, out var value)) - { - if (value is T typedValue) - { - return typedValue; - } - - // 尝试类型转换 - try - { - return (T)Convert.ChangeType(value, typeof(T)); - } - catch - { - return defaultValue; - } - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"获取插件 {pluginName} 配置 {key} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - - return defaultValue; - } - } - - /// - /// 设置插件配置值 - /// - /// 配置值类型 - /// 插件名称 - /// 配置键 - /// 配置值 - public static void SetConfiguration(string pluginName, string key, T value) - { - lock (_lockObject) - { - try - { - if (!_pluginConfigs.ContainsKey(pluginName)) - { - _pluginConfigs[pluginName] = new Dictionary(); - } - - _pluginConfigs[pluginName][key] = value; - - // 异步保存配置 - Task.Run(() => SavePluginConfiguration(pluginName)); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置插件 {pluginName} 配置 {key} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } - - /// - /// 删除插件配置 - /// - /// 插件名称 - /// 配置键 - public static void RemoveConfiguration(string pluginName, string key) - { - lock (_lockObject) - { - try - { - if (_pluginConfigs.TryGetValue(pluginName, out var pluginConfig)) - { - if (pluginConfig.Remove(key)) - { - // 异步保存配置 - Task.Run(() => SavePluginConfiguration(pluginName)); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"删除插件 {pluginName} 配置 {key} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } - - /// - /// 获取插件的所有配置 - /// - /// 插件名称 - /// 配置字典 - public static Dictionary GetAllConfigurations(string pluginName) - { - lock (_lockObject) - { - if (_pluginConfigs.TryGetValue(pluginName, out var pluginConfig)) - { - return new Dictionary(pluginConfig); - } - return new Dictionary(); - } - } - - /// - /// 清除插件的所有配置 - /// - /// 插件名称 - public static void ClearAllConfigurations(string pluginName) - { - lock (_lockObject) - { - try - { - if (_pluginConfigs.Remove(pluginName)) - { - // 删除配置文件 - string configFile = Path.Combine(PluginConfigDirectory, $"{pluginName}.json"); - if (File.Exists(configFile)) - { - File.Delete(configFile); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"清除插件 {pluginName} 所有配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } - - /// - /// 加载插件配置 - /// - /// 插件名称 - public static void LoadPluginConfiguration(string pluginName) - { - try - { - string configFile = Path.Combine(PluginConfigDirectory, $"{pluginName}.json"); - if (File.Exists(configFile)) - { - string json = File.ReadAllText(configFile); - var config = JsonConvert.DeserializeObject>(json); - - lock (_lockObject) - { - _pluginConfigs[pluginName] = config ?? new Dictionary(); - } - - LogHelper.WriteLogToFile($"已加载插件 {pluginName} 的配置"); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载插件 {pluginName} 配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 保存插件配置 - /// - /// 插件名称 - private static void SavePluginConfiguration(string pluginName) - { - try - { - Dictionary pluginConfig; - lock (_lockObject) - { - if (!_pluginConfigs.TryGetValue(pluginName, out pluginConfig)) - { - return; - } - } - - string configFile = Path.Combine(PluginConfigDirectory, $"{pluginName}.json"); - string json = JsonConvert.SerializeObject(pluginConfig, Formatting.Indented); - File.WriteAllText(configFile, json); - - LogHelper.WriteLogToFile($"已保存插件 {pluginName} 的配置"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存插件 {pluginName} 配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 加载所有插件的配置 - /// - public static void LoadAllPluginConfigurations() - { - try - { - if (Directory.Exists(PluginConfigDirectory)) - { - string[] configFiles = Directory.GetFiles(PluginConfigDirectory, "*.json"); - foreach (string configFile in configFiles) - { - string pluginName = Path.GetFileNameWithoutExtension(configFile); - LoadPluginConfiguration(pluginName); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载所有插件配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 保存所有插件的配置 - /// - public static void SaveAllPluginConfigurations() - { - try - { - lock (_lockObject) - { - foreach (string pluginName in _pluginConfigs.Keys) - { - SavePluginConfiguration(pluginName); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存所有插件配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/PluginManager.cs b/Ink Canvas/Helpers/Plugins/PluginManager.cs deleted file mode 100644 index 9e018183..00000000 --- a/Ink Canvas/Helpers/Plugins/PluginManager.cs +++ /dev/null @@ -1,1459 +0,0 @@ -using Ink_Canvas.Windows; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; -using Timer = System.Timers.Timer; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 插件管理器,负责插件的加载、卸载和管理 - /// - public class PluginManager - { - private static readonly string PluginsDirectory = Path.Combine(App.RootPath, "Plugins"); - private static readonly string PluginConfigFile = Path.Combine(App.RootPath, "Configs", "PluginConfig.json"); - private static readonly string PluginConfigBackupFile = Path.Combine(App.RootPath, "Configs", "PluginConfig.json.bak"); - - private static PluginManager _instance; - private static SemaphoreSlim _configLock = new SemaphoreSlim(1, 1); - - /// - /// 插件管理器单例 - /// - public static PluginManager Instance - { - get - { - if (_instance == null) - { - _instance = new PluginManager(); - } - return _instance; - } - } - - /// - /// 已加载的插件集合 - /// - public ObservableCollection Plugins { get; } = new ObservableCollection(); - - /// - /// 插件配置信息 - /// - public Dictionary PluginStates { get; private set; } = new Dictionary(); - - /// - /// 配置是否已更改但未保存 - /// - private bool _configDirty; - - /// - /// 配置自动保存计时器 - /// - private Timer _autoSaveTimer; - - /// - /// 加载的程序集缓存 - /// - private Dictionary _loadedAssemblies = new Dictionary(); - - /// - /// 插件文件哈希缓存,用于热重载检测 - /// - private Dictionary _pluginHashes = new Dictionary(); - - private PluginManager() - { - // 确保插件目录存在 - if (!Directory.Exists(PluginsDirectory)) - { - Directory.CreateDirectory(PluginsDirectory); - } - - - // 初始化自动保存计时器(3秒) - _autoSaveTimer = new Timer(3000); - _autoSaveTimer.Elapsed += (s, e) => - { - if (_configDirty) - { - SaveConfigAsync().ConfigureAwait(false); - } - }; - _autoSaveTimer.AutoReset = false; - - // 注册插件状态变更事件处理 - AppDomain.CurrentDomain.ProcessExit += (s, e) => - { - // 应用退出时强制保存配置 - if (_configDirty) - { - SaveConfig(); - } - }; - } - - /// - /// 初始化插件系统 - /// - public void Initialize() - { - try - { - LogHelper.WriteLogToFile("开始初始化插件系统"); - - // 加载配置 - LoadConfig(); - LogHelper.WriteLogToFile($"已从配置文件加载 {PluginStates.Count} 个插件状态记录"); - - // 加载内置插件 - LogHelper.WriteLogToFile("正在加载内置插件..."); - LoadBuiltInPlugins(); - - // 加载外部插件 - LogHelper.WriteLogToFile("正在加载外部插件..."); - LoadExternalPlugins(); - - // 启用已配置为启用的插件 - LogHelper.WriteLogToFile("正在应用配置的插件状态..."); - EnableConfiguredPlugins(); - - // 设置定期检查热重载 - StartHotReloadWatcher(); - - // 保存初始化后的配置(可能有新插件) - SaveConfig(); - - LogHelper.WriteLogToFile($"插件系统初始化完成,共加载 {Plugins.Count} 个插件"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"初始化插件系统时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 加载内置插件 - /// - private void LoadBuiltInPlugins() - { - try - { - // 获取当前程序集 - Assembly currentAssembly = Assembly.GetExecutingAssembly(); - - // 查找实现了IPlugin接口的所有类型 - var pluginTypes = currentAssembly.GetTypes() - .Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && t.IsClass); - - foreach (var pluginType in pluginTypes) - { - try - { - // 创建插件实例 - IPlugin plugin = (IPlugin)Activator.CreateInstance(pluginType); - - // 只处理内置插件 - if (plugin.IsBuiltIn) - { - plugin.Initialize(); - Plugins.Add(plugin); - LogHelper.WriteLogToFile($"已加载内置插件: {plugin.Name} v{plugin.Version}"); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载内置插件 {pluginType.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载内置插件时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 加载外部插件 - /// - private void LoadExternalPlugins() - { - try - { - // 检查插件目录是否存在 - if (!Directory.Exists(PluginsDirectory)) - { - Directory.CreateDirectory(PluginsDirectory); - return; - } - - // 获取所有插件文件(支持 .iccpp 和 .dll 格式) - var pluginFiles = Directory.GetFiles(PluginsDirectory, "*.iccpp", SearchOption.TopDirectoryOnly) - .Concat(Directory.GetFiles(PluginsDirectory, "*.dll", SearchOption.TopDirectoryOnly)) - .ToArray(); - - LogHelper.WriteLogToFile($"发现 {pluginFiles.Length} 个外部插件文件"); - - foreach (var pluginFile in pluginFiles) - { - LoadExternalPlugin(pluginFile); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载外部插件时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 加载单个外部插件 - /// - /// 插件文件路径 - /// 加载的插件实例,加载失败则返回null - public IPlugin LoadExternalPlugin(string pluginPath) - { - try - { - // 计算文件哈希 - string fileHash = CalculateFileHash(pluginPath); - _pluginHashes[pluginPath] = fileHash; - - // 检查文件扩展名 - string extension = Path.GetExtension(pluginPath).ToLowerInvariant(); - if (extension == ".iccpp") - { - // 创建 ICCPP 插件适配器 - return CreateICCPPPluginAdapter(pluginPath); - } - - // 加载插件程序集 - Assembly pluginAssembly = LoadPluginAssembly(pluginPath); - if (pluginAssembly == null) return null; - - // 查找实现了IPlugin接口的类型 - var pluginTypes = pluginAssembly.GetTypes() - .Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && t.IsClass); - - foreach (var pluginType in pluginTypes) - { - try - { - // 创建插件实例 - IPlugin plugin = (IPlugin)Activator.CreateInstance(pluginType); - - // 设置插件路径 - if (plugin is PluginBase pluginBase) - { - pluginBase.PluginPath = pluginPath; - } - - plugin.Initialize(); - Plugins.Add(plugin); - - LogHelper.WriteLogToFile($"已加载外部插件: {plugin.Name} v{plugin.Version} 来自 {Path.GetFileName(pluginPath)}"); - - return plugin; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"实例化插件类型 {pluginType.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - LogHelper.WriteLogToFile($"在程序集 {Path.GetFileName(pluginPath)} 中未找到有效的插件类型", LogHelper.LogType.Warning); - return null; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载外部插件 {Path.GetFileName(pluginPath)} 时出错: {ex.Message}", LogHelper.LogType.Error); - return null; - } - } - - /// - /// 创建 ICCPP 插件适配器 - /// - /// 插件文件路径 - /// 适配的插件实例 - private IPlugin CreateICCPPPluginAdapter(string pluginPath) - { - try - { - // 读取插件文件内容 - byte[] pluginData = File.ReadAllBytes(pluginPath); - - // 创建适配器插件实例 - var pluginAdapter = new ICCPPPluginAdapter(pluginPath, pluginData); - - // 添加到插件列表 - Plugins.Add(pluginAdapter); - - LogHelper.WriteLogToFile($"已创建 ICCPP 插件适配器: {pluginAdapter.Name} 来自 {Path.GetFileName(pluginPath)}"); - - return pluginAdapter; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"创建 ICCPP 插件适配器时出错: {ex.Message}", LogHelper.LogType.Error); - return null; - } - } - - /// - /// 加载插件程序集 - /// - /// 插件文件路径 - /// 加载的程序集 - private Assembly LoadPluginAssembly(string pluginPath) - { - try - { - // 检查是否已加载该程序集 - if (_loadedAssemblies.TryGetValue(pluginPath, out var loadedAssembly)) - { - return loadedAssembly; - } - - // 直接加载程序集 - Assembly pluginAssembly = Assembly.LoadFrom(pluginPath); - _loadedAssemblies[pluginPath] = pluginAssembly; - - return pluginAssembly; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载插件程序集 {Path.GetFileName(pluginPath)} 时出错: {ex.Message}", LogHelper.LogType.Error); - return null; - } - } - - /// - /// 启用已配置为启用的插件 - /// - private void EnableConfiguredPlugins() - { - int enabledCount = 0; - int disabledCount = 0; - int errorCount = 0; - - foreach (var plugin in Plugins) - { - try - { - string pluginTypeName = plugin.GetType().FullName; - - // 检查配置中的插件状态 - if (PluginStates.TryGetValue(pluginTypeName, out bool enabled)) - { - // 获取当前实际状态 - bool currentState = plugin is PluginBase pluginBase && pluginBase.IsEnabled; - - // 如果配置状态与当前状态不一致,则应用配置状态 - if (currentState != enabled) - { - // 注册插件状态变更事件 - if (plugin is PluginBase pb) - { - pb.EnabledStateChanged += Plugin_EnabledStateChanged; - } - - if (enabled) - { - plugin.Enable(); - enabledCount++; - LogHelper.WriteLogToFile($"根据配置启用插件: {plugin.Name}"); - } - else - { - plugin.Disable(); - disabledCount++; - LogHelper.WriteLogToFile($"根据配置禁用插件: {plugin.Name}"); - } - } - else - { - // 状态一致,只注册事件 - if (plugin is PluginBase pb) - { - pb.EnabledStateChanged += Plugin_EnabledStateChanged; - } - } - } - else - { - // 插件不在配置中,添加默认状态(禁用) - PluginStates[pluginTypeName] = false; - _configDirty = true; - - // 注册插件状态变更事件 - if (plugin is PluginBase pb) - { - pb.EnabledStateChanged += Plugin_EnabledStateChanged; - } - - // 如果当前是启用状态,则禁用 - if (plugin is PluginBase pluginBase && pluginBase.IsEnabled) - { - plugin.Disable(); - disabledCount++; - LogHelper.WriteLogToFile($"插件不在配置中,默认禁用: {plugin.Name}"); - } - } - } - catch (Exception ex) - { - errorCount++; - LogHelper.WriteLogToFile($"应用插件 {plugin.Name} 配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - // 如果有配置变更,启动自动保存 - if (_configDirty) - { - TriggerAutoSave(); - } - - LogHelper.WriteLogToFile($"已应用插件配置: 启用 {enabledCount} 个,禁用 {disabledCount} 个,错误 {errorCount} 个"); - } - - /// - /// 插件状态变更事件处理 - /// - private void Plugin_EnabledStateChanged(object sender, bool isEnabled) - { - try - { - if (sender is IPlugin plugin) - { - string pluginTypeName = plugin.GetType().FullName; - - // 更新配置状态 - if (!PluginStates.ContainsKey(pluginTypeName) || PluginStates[pluginTypeName] != isEnabled) - { - PluginStates[pluginTypeName] = isEnabled; - _configDirty = true; - - LogHelper.WriteLogToFile($"插件状态变更: {plugin.Name} = {(isEnabled ? "启用" : "禁用")}"); - - // 立即同步保存配置(不再使用延迟自动保存) - SaveConfig(); - LogHelper.WriteLogToFile($"插件 {plugin.Name} 状态已立即保存到配置文件"); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"处理插件状态变更事件时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 触发自动保存计时器 - /// - private void TriggerAutoSave() - { - // 重置并启动计时器 - _autoSaveTimer.Stop(); - _autoSaveTimer.Start(); - } - - /// - /// 启动热重载监视器 - /// - private void StartHotReloadWatcher() - { - // 创建定时检查任务 - Task.Run(async () => - { - while (true) - { - try - { - // 每5秒检查一次 - await Task.Delay(5000); - - // 获取所有外部插件 - var externalPlugins = Plugins.OfType() - .Where(p => !p.IsBuiltIn && !string.IsNullOrEmpty(p.PluginPath)) - .ToList(); - - foreach (var plugin in externalPlugins) - { - // 检查插件文件是否存在 - if (!File.Exists(plugin.PluginPath)) - { - continue; - } - - // 计算当前文件哈希 - string currentHash = CalculateFileHash(plugin.PluginPath); - - // 比较哈希值是否变化 - if (_pluginHashes.TryGetValue(plugin.PluginPath, out string oldHash) && - !string.IsNullOrEmpty(oldHash) && - oldHash != currentHash) - { - // 文件已变化,执行热重载 - Application.Current.Dispatcher.Invoke(() => - { - ReloadPlugin(plugin); - }); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"热重载检查出错: {ex.Message}", LogHelper.LogType.Error); - } - } - }); - } - - /// - /// 重新加载插件 - /// - /// 要重新加载的插件 - private void ReloadPlugin(PluginBase plugin) - { - try - { - string pluginPath = plugin.PluginPath; - if (string.IsNullOrEmpty(pluginPath) || !File.Exists(pluginPath)) - { - LogHelper.WriteLogToFile($"无法重新加载插件 {plugin.Name}: 插件文件不存在", LogHelper.LogType.Error); - return; - } - - LogHelper.WriteLogToFile($"开始热重载插件: {plugin.Name} ({Path.GetFileName(pluginPath)})"); - - // 保存插件的当前状态 - bool wasEnabled = plugin.IsEnabled; - string pluginTypeName = plugin.GetType().FullName; - - // 卸载插件 - UnloadPlugin(plugin); - - // 从加载缓存中移除 - if (_loadedAssemblies.ContainsKey(pluginPath)) - { - _loadedAssemblies.Remove(pluginPath); - } - - // 计算新的文件哈希 - string newHash = CalculateFileHash(pluginPath); - _pluginHashes[pluginPath] = newHash; - - // 重新加载插件 - IPlugin newPlugin = LoadExternalPlugin(pluginPath); - - if (newPlugin != null) - { - // 恢复插件状态 - if (wasEnabled) - { - newPlugin.Enable(); - } - - // 更新配置(如果类型名称变化) - string newPluginTypeName = newPlugin.GetType().FullName; - if (pluginTypeName != newPluginTypeName && PluginStates.ContainsKey(pluginTypeName)) - { - bool state = PluginStates[pluginTypeName]; - PluginStates.Remove(pluginTypeName); - PluginStates[newPluginTypeName] = state; - _configDirty = true; - SaveConfig(); - } - - LogHelper.WriteLogToFile($"插件 {newPlugin.Name} v{newPlugin.Version} 热重载成功"); - - // 通知UI刷新 - NotifyUIRefresh(); - } - else - { - LogHelper.WriteLogToFile($"插件 {plugin.Name} 热重载失败", LogHelper.LogType.Error); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"重新加载插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 卸载插件 - /// - /// 要卸载的插件 - /// 是否从配置中移除 - public void UnloadPlugin(IPlugin plugin, bool removeFromConfig = false) - { - try - { - // 保存插件名称,以便在卸载后使用 - string pluginName = plugin.Name; - - // 如果插件已启用,先禁用它 - if (plugin is PluginBase pluginBase && pluginBase.IsEnabled) - { - plugin.Disable(); - } - - // 执行插件清理 - plugin.Cleanup(); - - // 从插件集合中移除 - Plugins.Remove(plugin); - - // 从配置中移除(如果需要) - if (removeFromConfig && plugin.GetType() != null) - { - string pluginTypeName = plugin.GetType().FullName; - if (PluginStates.ContainsKey(pluginTypeName)) - { - PluginStates.Remove(pluginTypeName); - SaveConfig(); - } - } - - LogHelper.WriteLogToFile($"已卸载插件: {pluginName}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"卸载插件时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 删除插件 - /// - /// 要删除的插件 - /// 删除是否成功 - public bool DeletePlugin(IPlugin plugin) - { - try - { - // 只能删除外部插件 - if (plugin.IsBuiltIn) - { - return false; - } - - // 保存插件名称,以便在删除后使用 - string pluginName = plugin.Name; - - // 获取插件路径 - string pluginPath = null; - if (plugin is PluginBase pluginBase) - { - pluginPath = pluginBase.PluginPath; - } - - if (string.IsNullOrEmpty(pluginPath) || !File.Exists(pluginPath)) - { - return false; - } - - // 卸载插件(并从配置中移除状态) - UnloadPlugin(plugin, true); - - // 删除插件文件 - File.Delete(pluginPath); - - // 清理缓存 - _loadedAssemblies.Remove(pluginPath); - _pluginHashes.Remove(pluginPath); - - // 保存配置 - SaveConfig(); - - LogHelper.WriteLogToFile($"已删除插件: {pluginName}"); - return true; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"删除插件时出错: {ex.Message}", LogHelper.LogType.Error); - return false; - } - } - - /// - /// 切换插件启用状态 - /// - /// 目标插件 - /// 是否启用 - public void TogglePlugin(IPlugin plugin, bool enable) - { - try - { - // 检查当前状态是否已经是目标状态 - bool currentState = plugin is PluginBase pluginBase && pluginBase.IsEnabled; - if (currentState == enable) - { - // 已经是目标状态,无需操作 - LogHelper.WriteLogToFile($"插件 {plugin.Name} 已经是 {(enable ? "启用" : "禁用")} 状态,无需切换"); - return; - } - - // 记录插件信息,用于日志 - string pluginName = plugin.Name; - string pluginTypeName = plugin.GetType().FullName; - - LogHelper.WriteLogToFile($"开始切换插件 {pluginName} 状态为: {(enable ? "启用" : "禁用")}"); - - // 首先更新配置状态 - PluginStates[pluginTypeName] = enable; - _configDirty = true; - - // 更新插件状态 - try - { - // 注册事件(无需检查事件是否为null) - if (plugin is PluginBase pb) - { - // 先取消可能已有的订阅,避免重复订阅 - pb.EnabledStateChanged -= Plugin_EnabledStateChanged; - // 重新订阅 - pb.EnabledStateChanged += Plugin_EnabledStateChanged; - } - - // 更新插件状态 - if (enable) - { - plugin.Enable(); - LogHelper.WriteLogToFile($"插件 {pluginName} 已启用"); - } - else - { - // 禁用前先记录是否为内置插件 - bool isBuiltIn = plugin.IsBuiltIn; - LogHelper.WriteLogToFile($"尝试禁用{(isBuiltIn ? "内置" : "外部")}插件 {pluginName}"); - - // 禁用插件 - plugin.Disable(); - - // 禁用后立即检查状态,确保禁用成功 - bool actuallyDisabled = !(plugin is PluginBase pb2 && pb2.IsEnabled); - if (!actuallyDisabled) - { - LogHelper.WriteLogToFile($"警告: 插件 {pluginName} 禁用失败,再次尝试禁用", LogHelper.LogType.Warning); - plugin.Disable(); // 再次尝试禁用 - - // 再次检查 - actuallyDisabled = !(plugin is PluginBase pb3 && pb3.IsEnabled); - if (!actuallyDisabled) - { - LogHelper.WriteLogToFile($"错误: 插件 {pluginName} 禁用失败,强制设置禁用状态", LogHelper.LogType.Error); - // 强制设置状态 - if (plugin is PluginBase pb4) - { - // 使用反射强制设置禁用状态 - var enabledProperty = typeof(PluginBase).GetProperty("IsEnabled"); - if (enabledProperty != null) - { - enabledProperty.SetValue(pb4, false); - LogHelper.WriteLogToFile($"已通过反射强制设置插件 {pluginName} 为禁用状态"); - } - } - } - } - - LogHelper.WriteLogToFile($"插件 {pluginName} 已禁用"); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"更改插件 {pluginName} 状态时出错: {ex.Message}", LogHelper.LogType.Error); - } - - // 立即保存配置 - SaveConfigAsync().ConfigureAwait(false); - - // 插件状态切换后,始终进行重载(无论是启用还是禁用) - if (plugin is PluginBase pluginInstance) - { - // 对于内置插件,执行专门的处理 - if (pluginInstance.IsBuiltIn) - { - LogHelper.WriteLogToFile($"处理内置插件 {pluginName} 状态变更"); - - // 对于内置插件,我们需要确保状态正确应用 - bool finalState = pluginInstance.IsEnabled; - bool expectedState = enable; - - if (finalState != expectedState) - { - LogHelper.WriteLogToFile($"内置插件状态不匹配: 当前={finalState}, 期望={expectedState},尝试纠正", LogHelper.LogType.Warning); - - // 再次尝试设置状态 - if (expectedState) - { - plugin.Enable(); - } - else - { - plugin.Disable(); - - // 最后一次检查,如果仍然不匹配,强制设置 - if (pluginInstance.IsEnabled != expectedState) - { - var enabledProperty = typeof(PluginBase).GetProperty("IsEnabled"); - if (enabledProperty != null) - { - enabledProperty.SetValue(pluginInstance, expectedState); - LogHelper.WriteLogToFile($"已通过反射强制设置内置插件 {pluginName} 状态为 {(expectedState ? "启用" : "禁用")}"); - } - } - } - } - - // 通知UI刷新 - NotifyUIRefresh(); - } - else - { - // 外部插件,执行热重载 - try - { - if (!string.IsNullOrEmpty(pluginInstance.PluginPath) && File.Exists(pluginInstance.PluginPath)) - { - LogHelper.WriteLogToFile($"开始重载外部插件 {pluginName}"); - - // 使用调度器确保在UI线程执行热重载 - if (Application.Current != null && Application.Current.Dispatcher != null) - { - Application.Current.Dispatcher.BeginInvoke(new Action(() => - { - ReloadPlugin(pluginInstance); - LogHelper.WriteLogToFile($"插件 {pluginName} 已重载以应用{(enable ? "启用" : "禁用")}状态"); - })); - } - else - { - // 当前不在UI线程,直接重载 - ReloadPlugin(pluginInstance); - LogHelper.WriteLogToFile($"插件 {pluginName} 已重载以应用{(enable ? "启用" : "禁用")}状态"); - } - } - else - { - LogHelper.WriteLogToFile($"外部插件 {pluginName} 文件不存在,无法重载", LogHelper.LogType.Warning); - NotifyUIRefresh(); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"重载插件 {pluginName} 时出错: {ex.Message}", LogHelper.LogType.Error); - // 出错时也要刷新UI - NotifyUIRefresh(); - } - } - } - else - { - // 通知UI刷新 - NotifyUIRefresh(); - } - - LogHelper.WriteLogToFile($"插件 {pluginName} 状态切换完成"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"切换插件状态时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 应用插件实时状态 - /// - /// 目标插件 - /// 是否启用 - private void ApplyPluginRealTimeState(IPlugin plugin, bool enable) - { - try - { - // 确保当前实例状态正确 - bool currentState = plugin is PluginBase pluginBase && pluginBase.IsEnabled; - if (currentState != enable) - { - if (enable) - { - plugin.Enable(); - LogHelper.WriteLogToFile($"实时应用: 已启用插件 {plugin.Name}"); - } - else - { - plugin.Disable(); - LogHelper.WriteLogToFile($"实时应用: 已禁用插件 {plugin.Name}"); - } - - // 同步状态到插件自身的配置 - if (plugin is PluginBase pluginSettings) - { - try - { - // 保存插件设置(与启用状态无关) - pluginSettings.SavePluginSettings(); - LogHelper.WriteLogToFile($"实时应用: 已保存插件 {plugin.Name} 设置"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"实时应用: 保存插件 {plugin.Name} 设置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } - - // 对于外部插件,尝试执行热重载以确保状态立即生效 - if (plugin is PluginBase externalPlugin && !externalPlugin.IsBuiltIn) - { - string pluginPath = externalPlugin.PluginPath; - if (!string.IsNullOrEmpty(pluginPath) && File.Exists(pluginPath)) - { - // 记录插件类型名称,用于后续状态检查 - string pluginTypeName = plugin.GetType().FullName; - bool targetState = enable; - - // 使用调度器确保在UI线程执行热重载 - if (Application.Current != null && Application.Current.Dispatcher != null) - { - Application.Current.Dispatcher.BeginInvoke(new Action(() => - { - try - { - // 热重载前再次确认配置状态正确 - if (PluginStates.TryGetValue(pluginTypeName, out bool storedStateUi) && storedStateUi != targetState) - { - LogHelper.WriteLogToFile($"热重载前发现状态不一致,修正配置: {plugin.Name}, 配置={storedStateUi}, 目标={targetState}", LogHelper.LogType.Warning); - PluginStates[pluginTypeName] = targetState; - SaveConfig(); - } - - // 执行热重载 - ReloadPlugin(externalPlugin); - LogHelper.WriteLogToFile($"插件 {plugin.Name} 已成功热重载以应用实时状态"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"热重载插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - })); - } - else - { - // 当前不在UI线程,直接重载 - // 热重载前再次确认配置状态正确 - if (PluginStates.TryGetValue(pluginTypeName, out bool storedStateNonUi) && storedStateNonUi != targetState) - { - LogHelper.WriteLogToFile($"热重载前发现状态不一致,修正配置: {plugin.Name}, 配置={storedStateNonUi}, 目标={targetState}", LogHelper.LogType.Warning); - PluginStates[pluginTypeName] = targetState; - SaveConfig(); - } - - ReloadPlugin(externalPlugin); - } - } - } - - LogHelper.WriteLogToFile($"插件 {plugin.Name} 实时状态已应用: {(enable ? "启用" : "禁用")}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"应用插件实时状态时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 通知UI刷新 - /// - private void NotifyUIRefresh() - { - try - { - // 通知UI刷新 - if (Application.Current != null && Application.Current.Dispatcher != null) - { - Application.Current.Dispatcher.BeginInvoke(new Action(() => - { - // 通知任何可能打开的插件设置窗口刷新 - foreach (Window window in Application.Current.Windows) - { - if (window is PluginSettingsWindow pluginWindow) - { - pluginWindow.RefreshPluginList(); - break; - } - } - })); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"通知UI刷新时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 加载插件配置 - /// - private void LoadConfig() - { - const int maxRetries = 3; // 最大重试次数 - const int retryDelayMs = 300; // 重试延迟时间(毫秒) - - LogHelper.WriteLogToFile($"开始从配置文件加载插件状态: {PluginConfigFile}"); - - // 确保至少有一个默认配置 - Dictionary defaultConfig = new Dictionary(); - - // 尝试获取配置锁 - _configLock.Wait(); - - try - { - for (int attempt = 1; attempt <= maxRetries; attempt++) - { - try - { - if (File.Exists(PluginConfigFile)) - { - string json; - // 使用共享读取模式,允许其他进程同时读取但不允许写入 - using (FileStream fs = new FileStream(PluginConfigFile, FileMode.Open, FileAccess.Read, FileShare.Read)) - using (StreamReader reader = new StreamReader(fs)) - { - json = reader.ReadToEnd(); - } - - var loadedStates = JsonConvert.DeserializeObject>(json); - - if (loadedStates != null && loadedStates.Count > 0) - { - PluginStates = loadedStates; - _configDirty = false; // 重置脏标记 - LogHelper.WriteLogToFile($"成功从配置文件加载了 {PluginStates.Count} 个插件状态"); - } - else - { - LogHelper.WriteLogToFile("配置文件解析为空,尝试使用备份", LogHelper.LogType.Warning); - // 尝试加载备份 - if (File.Exists(PluginConfigBackupFile)) - { - try - { - string backupJson = File.ReadAllText(PluginConfigBackupFile); - var backupStates = JsonConvert.DeserializeObject>(backupJson); - - if (backupStates != null && backupStates.Count > 0) - { - PluginStates = backupStates; - _configDirty = true; // 从备份加载,需要重新保存主配置 - LogHelper.WriteLogToFile($"已从备份恢复 {PluginStates.Count} 个插件状态"); - return; // 成功从备份加载,提前退出 - } - } - catch (Exception backupEx) - { - LogHelper.WriteLogToFile($"从备份恢复配置失败: {backupEx.Message}", LogHelper.LogType.Error); - } - } - - // 备份也失败,使用默认配置 - PluginStates = defaultConfig; - _configDirty = true; - } - } - else - { - LogHelper.WriteLogToFile($"配置文件不存在,尝试使用备份: {PluginConfigFile}", LogHelper.LogType.Warning); - - // 尝试加载备份 - if (File.Exists(PluginConfigBackupFile)) - { - try - { - string backupJson = File.ReadAllText(PluginConfigBackupFile); - var backupStates = JsonConvert.DeserializeObject>(backupJson); - - if (backupStates != null && backupStates.Count > 0) - { - PluginStates = backupStates; - _configDirty = true; // 从备份加载,需要重新保存主配置 - LogHelper.WriteLogToFile($"已从备份恢复 {PluginStates.Count} 个插件状态"); - return; // 成功从备份加载,提前退出 - } - } - catch (Exception backupEx) - { - LogHelper.WriteLogToFile($"从备份恢复配置失败: {backupEx.Message}", LogHelper.LogType.Error); - } - } - - PluginStates = defaultConfig; - _configDirty = true; - LogHelper.WriteLogToFile("使用默认空配置", LogHelper.LogType.Warning); - } - - // 没有成功加载或使用备份,使用默认配置 - break; - } - catch (Exception ex) - { - if (attempt < maxRetries) - { - LogHelper.WriteLogToFile($"加载配置失败 (尝试 {attempt}/{maxRetries}): {ex.Message},将在 {retryDelayMs}ms 后重试", LogHelper.LogType.Warning); - Thread.Sleep(retryDelayMs); - } - else - { - LogHelper.WriteLogToFile($"加载插件配置失败,已达最大重试次数 ({maxRetries}): {ex.Message}", LogHelper.LogType.Error); - - // 最终失败,使用默认配置 - PluginStates = defaultConfig; - _configDirty = true; - } - } - } - } - finally - { - // 释放配置锁 - _configLock.Release(); - } - } - - /// - /// 异步保存插件配置 - /// - public async Task SaveConfigAsync() - { - // 如果配置没有变化,无需保存 - if (!_configDirty) - { - return; - } - - // 尝试获取配置锁(异步) - if (!await _configLock.WaitAsync(0)) - { - // 已有保存操作在进行中,触发自动保存延迟 - TriggerAutoSave(); - return; - } - - try - { - // 创建配置任务 - await Task.Run(() => SaveConfig()); - } - finally - { - // 释放配置锁 - _configLock.Release(); - } - } - - /// - /// 保存插件配置 - /// - public void SaveConfig() - { - // 如果配置没有变化,无需保存 - if (!_configDirty) - { - return; - } - - const int maxRetries = 3; // 最大重试次数 - const int retryDelayMs = 500; // 重试延迟时间(毫秒) - - try - { - LogHelper.WriteLogToFile($"开始保存插件配置到: {PluginConfigFile}"); - - // 生成JSON数据 - string json = JsonConvert.SerializeObject(PluginStates, Formatting.Indented); - string tempFile = PluginConfigFile + ".temp"; // 临时文件路径 - - // 确保目录存在 - string configDir = Path.GetDirectoryName(PluginConfigFile); - if (!Directory.Exists(configDir)) - { - Directory.CreateDirectory(configDir); - LogHelper.WriteLogToFile($"创建配置目录: {configDir}"); - } - - // 先备份当前配置 - try - { - if (File.Exists(PluginConfigFile)) - { - File.Copy(PluginConfigFile, PluginConfigBackupFile, true); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"备份配置文件失败: {ex.Message}", LogHelper.LogType.Warning); - } - - for (int attempt = 1; attempt <= maxRetries; attempt++) - { - try - { - // 直接写入目标文件 - File.WriteAllText(PluginConfigFile, json); - - // 验证写入是否成功 - if (File.Exists(PluginConfigFile)) - { - // 重置脏标记 - _configDirty = false; - LogHelper.WriteLogToFile($"插件配置已成功保存到磁盘: {PluginConfigFile}, 共 {PluginStates.Count} 个插件状态"); - return; - } - } - catch (Exception ex) - { - if (attempt < maxRetries) - { - LogHelper.WriteLogToFile($"保存配置失败 (尝试 {attempt}/{maxRetries}): {ex.Message},将在 {retryDelayMs}ms 后重试", LogHelper.LogType.Warning); - Thread.Sleep(retryDelayMs); - } - else - { - LogHelper.WriteLogToFile($"保存插件配置失败,已达最大重试次数 ({maxRetries}): {ex.Message}", LogHelper.LogType.Error); - - // 尝试使用临时文件方式 - try - { - // 删除可能存在的旧临时文件 - if (File.Exists(tempFile)) - { - File.Delete(tempFile); - } - - // 写入临时文件 - File.WriteAllText(tempFile, json); - - // 如果目标文件存在,先删除 - if (File.Exists(PluginConfigFile)) - { - File.Delete(PluginConfigFile); - } - - // 重命名临时文件 - File.Move(tempFile, PluginConfigFile); - - // 重置脏标记 - _configDirty = false; - LogHelper.WriteLogToFile($"使用临时文件方式成功保存配置: {PluginConfigFile}"); - return; - } - catch (Exception fallbackEx) - { - LogHelper.WriteLogToFile($"临时文件保存方式也失败: {fallbackEx.Message}", LogHelper.LogType.Error); - } - } - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存插件配置时发生未处理异常: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 计算文件哈希 - /// - /// 文件路径 - /// 文件哈希值 - private string CalculateFileHash(string filePath) - { - try - { - using (var md5 = MD5.Create()) - using (var stream = File.OpenRead(filePath)) - { - byte[] hash = md5.ComputeHash(stream); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"计算文件哈希值时出错: {ex.Message}", LogHelper.LogType.Error); - return string.Empty; - } - } - - /// - /// 从配置文件重新加载所有插件状态并应用 - /// - public void ReloadPluginsFromConfig() - { - try - { - LogHelper.WriteLogToFile("开始从配置文件重新加载插件状态"); - - // 保存当前配置状态,以便在加载失败时回滚 - Dictionary previousStates = new Dictionary(PluginStates); - - // 重新加载配置文件 - LoadConfig(); - - // 如果配置文件加载失败,PluginStates可能为空,这时使用之前的状态 - if (PluginStates == null || PluginStates.Count == 0) - { - LogHelper.WriteLogToFile("加载的配置为空,恢复到之前的状态", LogHelper.LogType.Warning); - PluginStates = previousStates; - return; - } - - LogHelper.WriteLogToFile($"已加载 {PluginStates.Count} 个插件状态,开始应用..."); - - // 对比配置,查找变更的插件 - foreach (var plugin in Plugins.ToList()) // 创建副本进行遍历,避免集合修改异常 - { - string pluginTypeName = plugin.GetType().FullName; - - // 检查插件在配置中是否存在 - if (PluginStates.TryGetValue(pluginTypeName, out bool shouldBeEnabled)) - { - bool currentlyEnabled = plugin is PluginBase pluginBase && pluginBase.IsEnabled; - - // 如果状态需要变更 - if (currentlyEnabled != shouldBeEnabled) - { - LogHelper.WriteLogToFile($"应用插件 {plugin.Name} 的配置状态: {(shouldBeEnabled ? "启用" : "禁用")}"); - - if (shouldBeEnabled) - { - try - { - plugin.Enable(); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启用插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - else - { - try - { - // 记录禁用信息,特别是内置插件 - bool isBuiltIn = plugin.IsBuiltIn; - LogHelper.WriteLogToFile($"尝试禁用{(isBuiltIn ? "内置" : "外部")}插件 {plugin.Name}"); - - // 禁用插件 - plugin.Disable(); - - // 对于内置插件,特别检查禁用状态 - if (isBuiltIn && plugin is PluginBase builtInPluginBase) - { - if (builtInPluginBase.IsEnabled) - { - LogHelper.WriteLogToFile($"内置插件 {plugin.Name} 禁用失败,尝试强制禁用", LogHelper.LogType.Warning); - // 强制设置禁用状态 - var enabledProperty = typeof(PluginBase).GetProperty("IsEnabled"); - if (enabledProperty != null) - { - enabledProperty.SetValue(builtInPluginBase, false); - LogHelper.WriteLogToFile($"已通过反射强制禁用内置插件 {plugin.Name}"); - } - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"禁用插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - // 如果是外部插件,执行重载 - if (!plugin.IsBuiltIn && plugin is PluginBase externalPlugin && !string.IsNullOrEmpty(externalPlugin.PluginPath)) - { - try - { - ReloadPlugin(externalPlugin); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"重载外部插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } - } - else - { - // 插件不在配置中,将其添加为禁用状态 - PluginStates[pluginTypeName] = false; - LogHelper.WriteLogToFile($"插件 {plugin.Name} 不在配置中,默认设置为禁用状态"); - - // 如果当前是启用状态,则禁用它 - if (plugin is PluginBase pluginBase && pluginBase.IsEnabled) - { - try - { - bool isBuiltIn = plugin.IsBuiltIn; - LogHelper.WriteLogToFile($"尝试禁用未配置的{(isBuiltIn ? "内置" : "外部")}插件 {plugin.Name}"); - - plugin.Disable(); - - // 对于内置插件,特别检查禁用状态 - if (isBuiltIn && pluginBase.IsEnabled) - { - LogHelper.WriteLogToFile($"未配置的内置插件 {plugin.Name} 禁用失败,尝试强制禁用", LogHelper.LogType.Warning); - // 强制设置禁用状态 - var enabledProperty = typeof(PluginBase).GetProperty("IsEnabled"); - if (enabledProperty != null) - { - enabledProperty.SetValue(pluginBase, false); - LogHelper.WriteLogToFile($"已通过反射强制禁用未配置的内置插件 {plugin.Name}"); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"禁用未配置插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } - } - - // 保存更新后的配置 - SaveConfig(); - - // 通知UI更新 - if (Application.Current != null && Application.Current.Dispatcher != null) - { - Application.Current.Dispatcher.Invoke(() => - { - // 通知任何可能打开的插件设置窗口刷新 - foreach (Window window in Application.Current.Windows) - { - if (window is PluginSettingsWindow pluginWindow) - { - pluginWindow.RefreshPluginList(); - } - } - }); - } - - LogHelper.WriteLogToFile("插件状态已从配置文件重新加载完成"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"从配置文件重新加载插件状态时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/PluginServiceManager.cs b/Ink Canvas/Helpers/Plugins/PluginServiceManager.cs deleted file mode 100644 index 9975998e..00000000 --- a/Ink Canvas/Helpers/Plugins/PluginServiceManager.cs +++ /dev/null @@ -1,509 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 插件服务管理器,实现IPluginService接口,提供对软件内部功能的访问 - /// - public class PluginServiceManager : IPluginService - { - private static PluginServiceManager _instance; - private MainWindow _mainWindow; - private Dictionary _eventHandlers; - - /// - /// 单例实例 - /// - public static PluginServiceManager Instance - { - get - { - if (_instance == null) - { - _instance = new PluginServiceManager(); - } - return _instance; - } - } - - private PluginServiceManager() - { - _eventHandlers = new Dictionary(); - } - - /// - /// 设置主窗口引用 - /// - /// 主窗口实例 - public void SetMainWindow(MainWindow mainWindow) - { - _mainWindow = mainWindow; - } - - #region 窗口和UI访问 - - public Window MainWindow => _mainWindow; - - public InkCanvas CurrentCanvas => null; // 暂时返回null,避免访问权限问题 - - public List AllCanvasPages => new List(); // 暂时返回空列表 - - public int CurrentPageIndex => 0; // 暂时返回0 - - public int TotalPageCount => 0; // 暂时返回0 - - public FrameworkElement FloatingToolBar => _mainWindow?.ViewboxFloatingBar; - - public FrameworkElement LeftPanel => _mainWindow?.BlackboardLeftSide; - - public FrameworkElement RightPanel => _mainWindow?.BlackboardRightSide; - - public FrameworkElement TopPanel => _mainWindow?.BorderTools; - - public FrameworkElement BottomPanel => _mainWindow?.BorderSettings; - - #endregion - - #region 绘制工具状态 - - public int CurrentDrawingMode => 0; // 暂时返回0 - - public double CurrentInkWidth => 2.5; // 暂时返回默认值 - - public Color CurrentInkColor => Colors.Black; // 暂时返回默认值 - - public double CurrentHighlighterWidth => 20.0; // 暂时返回默认值 - - public int CurrentEraserSize => 2; // 暂时返回默认值 - - public int CurrentEraserType => 0; // 暂时返回默认值 - - public int CurrentEraserShape => 0; // 暂时返回默认值 - - public double CurrentInkAlpha => 255.0; // 暂时返回默认值 - - public int CurrentInkStyle => 0; // 暂时返回默认值 - - public string CurrentBackgroundColor => "#162924"; // 暂时返回默认值 - - #endregion - - #region 应用状态 - - public bool IsDarkTheme => false; // 暂时返回默认值 - - public bool IsWhiteboardMode => false; // 暂时返回默认值 - - public bool IsPPTMode => false; // 暂时返回默认值 - - public bool IsFullScreenMode => false; // 暂时返回默认值 - - public bool IsCanvasMode => true; // 暂时返回默认值 - - public bool IsSelectionMode => false; // 暂时返回默认值 - - public bool IsEraserMode => false; // 暂时返回默认值 - - public bool IsShapeDrawingMode => false; // 暂时返回默认值 - - public bool IsHighlighterMode => false; // 暂时返回默认值 - - #endregion - - #region IGetService 实现 - - public bool CanUndo => false; // 暂时返回默认值 - - public bool CanRedo => false; // 暂时返回默认值 - - public T GetSetting(string key, T defaultValue = default(T)) - { - // 暂时不实现,避免访问权限问题 - return defaultValue; - } - - public List GetAllPlugins() - { - return new List(PluginManager.Instance.Plugins); - } - - public IPlugin GetPlugin(string pluginName) - { - return PluginManager.Instance.Plugins.FirstOrDefault(p => p.Name == pluginName); - } - - #endregion - - #region IWindowService 实现 - - public void ShowSettingsWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void HideSettingsWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void ShowPluginSettingsWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void HidePluginSettingsWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void ShowHelpWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void HideHelpWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void ShowAboutWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void HideAboutWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void ShowNotification(string message, NotificationType type = NotificationType.Info) - { - // 暂时不实现,避免访问权限问题 - } - - public bool ShowConfirmDialog(string message, string title = "确认") - { - // 暂时不实现,避免访问权限问题 - return false; - } - - public string ShowInputDialog(string message, string title = "输入", string defaultValue = "") - { - // 暂时不实现,避免访问权限问题 - return defaultValue; - } - - public void SetFullScreen(bool isFullScreen) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetTopMost(bool isTopMost) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetWindowVisibility(bool isVisible) - { - // 暂时不实现,避免访问权限问题 - } - - public void MinimizeWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void MaximizeWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void RestoreWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void CloseWindow() - { - // 暂时不实现,避免访问权限问题 - } - - public void SetWindowPosition(double x, double y) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetWindowSize(double width, double height) - { - // 暂时不实现,避免访问权限问题 - } - - public (double x, double y) GetWindowPosition() - { - // 暂时不实现,避免访问权限问题 - return (0, 0); - } - - public (double width, double height) GetWindowSize() - { - // 暂时不实现,避免访问权限问题 - return (800, 600); - } - - #endregion - - #region IActionService 实现 - - public void ClearCanvas() - { - // 暂时不实现,避免访问权限问题 - } - - public void ClearAllCanvases() - { - // 暂时不实现,避免访问权限问题 - } - - public void AddNewPage() - { - // 暂时不实现,避免访问权限问题 - } - - public void DeleteCurrentPage() - { - // 暂时不实现,避免访问权限问题 - } - - public void SwitchToPage(int pageIndex) - { - // 暂时不实现,避免访问权限问题 - } - - public void NextPage() - { - // 暂时不实现,避免访问权限问题 - } - - public void PreviousPage() - { - // 暂时不实现,避免访问权限问题 - } - - public void SetDrawingMode(int mode) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetInkWidth(double width) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetInkColor(Color color) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetHighlighterWidth(double width) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetEraserSize(int size) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetEraserType(int type) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetEraserShape(int shape) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetInkAlpha(double alpha) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetInkStyle(int style) - { - // 暂时不实现,避免访问权限问题 - } - - public void SetBackgroundColor(string color) - { - // 暂时不实现,避免访问权限问题 - } - - public void SaveCanvas(string filePath) - { - // 暂时不实现,避免访问权限问题 - } - - public void LoadCanvas(string filePath) - { - // 暂时不实现,避免访问权限问题 - } - - public void ExportAsImage(string filePath, string format) - { - // 暂时不实现,避免访问权限问题 - } - - public void ExportAsPDF(string filePath) - { - // 暂时不实现,避免访问权限问题 - } - - public void Undo() - { - // 暂时不实现,避免访问权限问题 - } - - public void Redo() - { - // 暂时不实现,避免访问权限问题 - } - - public void SelectAll() - { - // 暂时不实现,避免访问权限问题 - } - - public void DeselectAll() - { - // 暂时不实现,避免访问权限问题 - } - - public void DeleteSelected() - { - // 暂时不实现,避免访问权限问题 - } - - public void CopySelected() - { - // 暂时不实现,避免访问权限问题 - } - - public void CutSelected() - { - // 暂时不实现,避免访问权限问题 - } - - public void Paste() - { - // 暂时不实现,避免访问权限问题 - } - - public void SetSetting(string key, T value) - { - // 暂时不实现,避免访问权限问题 - } - - public void SaveSettings() - { - // 暂时不实现,避免访问权限问题 - } - - public void LoadSettings() - { - // 暂时不实现,避免访问权限问题 - } - - public void ResetSettings() - { - // 暂时不实现,避免访问权限问题 - } - - public void EnablePlugin(string pluginName) - { - var plugin = GetPlugin(pluginName); - if (plugin != null) - { - PluginManager.Instance.TogglePlugin(plugin, true); - } - } - - public void DisablePlugin(string pluginName) - { - var plugin = GetPlugin(pluginName); - if (plugin != null) - { - PluginManager.Instance.TogglePlugin(plugin, false); - } - } - - public void UnloadPlugin(string pluginName) - { - var plugin = GetPlugin(pluginName); - if (plugin != null) - { - PluginManager.Instance.UnloadPlugin(plugin); - } - } - - public void RegisterEventHandler(string eventName, EventHandler handler) - { - if (!_eventHandlers.ContainsKey(eventName)) - { - _eventHandlers[eventName] = handler; - } - else - { - _eventHandlers[eventName] += handler; - } - } - - public void UnregisterEventHandler(string eventName, EventHandler handler) - { - if (_eventHandlers.ContainsKey(eventName)) - { - _eventHandlers[eventName] -= handler; - } - } - - public void TriggerEvent(string eventName, object sender, EventArgs args) - { - if (_eventHandlers.ContainsKey(eventName)) - { - _eventHandlers[eventName]?.Invoke(sender, args); - } - } - - public void RestartApplication() - { - // 暂时不实现,避免访问权限问题 - } - - public void ExitApplication() - { - // 暂时不实现,避免访问权限问题 - } - - public void CheckForUpdates() - { - // 暂时不实现,避免访问权限问题 - } - - public void OpenHelpDocument() - { - // 暂时不实现,避免访问权限问题 - } - - public void OpenAboutPage() - { - // 暂时不实现,避免访问权限问题 - } - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/PluginTemplate.cs b/Ink Canvas/Helpers/Plugins/PluginTemplate.cs deleted file mode 100644 index bd6979d8..00000000 --- a/Ink Canvas/Helpers/Plugins/PluginTemplate.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System; -using System.Windows; -using System.Windows.Controls; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 插件模板,用于开发者参考 - /// 注意:实际开发时,请将此类移到单独的程序集中 - /// - public class PluginTemplate : PluginBase - { - #region 插件基本信息 - - /// - /// 插件名称 - /// - public override string Name => "插件模板"; - - /// - /// 插件描述 - /// - public override string Description => "这是一个插件开发模板,用于开发者参考。"; - - /// - /// 插件版本 - /// - public override Version Version => new Version(1, 0, 0); - - /// - /// 插件作者 - /// - public override string Author => "Your Name"; - - /// - /// 是否为内置插件(外部插件请返回false) - /// - public override bool IsBuiltIn => false; - - #endregion - - #region 插件生命周期 - - /// - /// 插件初始化 - /// 在这里进行插件的初始化工作,如加载配置、注册事件等 - /// - public override void Initialize() - { - // 先调用基类方法,这样会设置插件ID和记录日志 - base.Initialize(); - - // TODO: 在这里进行插件初始化工作 - - // 示例:记录初始化信息 - LogHelper.WriteLogToFile($"插件 {Name} 开始初始化"); - - // 示例:加载配置 - LoadConfig(); - - // 示例:注册自定义事件 - // MainWindow.Instance.SomeEvent += OnSomeEvent; - - LogHelper.WriteLogToFile($"插件 {Name} 初始化完成"); - } - - /// - /// 启用插件 - /// 在这里激活插件功能 - /// - public override void Enable() - { - // 先调用基类方法,这样会设置插件状态和记录日志 - base.Enable(); - - // TODO: 在这里启用插件功能 - - LogHelper.WriteLogToFile($"插件 {Name} 已启用"); - } - - /// - /// 禁用插件 - /// 在这里停用插件功能 - /// - public override void Disable() - { - // 先调用基类方法,这样会设置插件状态和记录日志 - base.Disable(); - - // TODO: 在这里禁用插件功能 - - LogHelper.WriteLogToFile($"插件 {Name} 已禁用"); - } - - /// - /// 清理资源 - /// 在插件卸载时调用,清理资源 - /// - public override void Cleanup() - { - // TODO: 在这里清理插件资源 - - // 示例:取消注册事件 - // MainWindow.Instance.SomeEvent -= OnSomeEvent; - - // 示例:保存配置 - SaveConfig(); - - // 最后调用基类方法 - base.Cleanup(); - } - - #endregion - - #region 插件配置 - - /// - /// 加载插件配置 - /// - private void LoadConfig() - { - try - { - // TODO: 从文件或其他位置加载配置 - // 示例: - // string configPath = Path.Combine(App.RootPath, "PluginConfigs", "YourPluginName.json"); - // if (File.Exists(configPath)) - // { - // string json = File.ReadAllText(configPath); - // YourConfig = Newtonsoft.Json.JsonConvert.DeserializeObject(json); - // } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载插件配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 保存插件配置 - /// - private void SaveConfig() - { - try - { - // TODO: 保存配置到文件或其他位置 - // 示例: - // string configDir = Path.Combine(App.RootPath, "PluginConfigs"); - // if (!Directory.Exists(configDir)) - // { - // Directory.CreateDirectory(configDir); - // } - // string configPath = Path.Combine(configDir, "YourPluginName.json"); - // string json = Newtonsoft.Json.JsonConvert.SerializeObject(YourConfig, Newtonsoft.Json.Formatting.Indented); - // File.WriteAllText(configPath, json); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存插件配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - - #region 插件设置界面 - - /// - /// 获取插件设置界面 - /// - /// 插件设置界面 - public override UserControl GetSettingsView() - { - // 创建插件设置界面 - return new PluginTemplateSettingsControl(); - } - - #endregion - - #region 插件功能方法 - - // TODO: 在这里添加插件的具体功能方法 - - /// - /// 示例方法:执行一些功能 - /// - public void DoSomething() - { - if (!IsEnabled) return; - - try - { - // TODO: 实现你的功能 - MessageBox.Show("插件功能执行示例", "插件模板", MessageBoxButton.OK, MessageBoxImage.Information); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"执行插件功能时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - } - - /// - /// 插件设置控件 - /// - public class PluginTemplateSettingsControl : UserControl - { - public PluginTemplateSettingsControl() - { - // 创建设置界面布局 - var panel = new StackPanel - { - Margin = new Thickness(10) - }; - - // 添加标题 - panel.Children.Add(new TextBlock - { - Text = "插件模板设置", - FontSize = 16, - FontWeight = FontWeights.Bold, - Margin = new Thickness(0, 0, 0, 10) - }); - - // 添加说明文字 - panel.Children.Add(new TextBlock - { - Text = "这是一个示例设置界面,你可以在这里添加自己的设置控件。", - TextWrapping = TextWrapping.Wrap, - Margin = new Thickness(0, 0, 0, 15) - }); - - // 添加示例设置选项 - var checkBox = new CheckBox - { - Content = "启用某项功能", - Margin = new Thickness(0, 0, 0, 10) - }; - panel.Children.Add(checkBox); - - // 添加文本输入框 - panel.Children.Add(new TextBlock - { - Text = "设置项:", - Margin = new Thickness(0, 5, 0, 5) - }); - - panel.Children.Add(new TextBox - { - Margin = new Thickness(0, 0, 0, 10), - Width = 200, - HorizontalAlignment = HorizontalAlignment.Left - }); - - // 添加按钮 - var button = new Button - { - Content = "保存设置", - Padding = new Thickness(10, 5, 10, 5), - Margin = new Thickness(0, 10, 0, 0), - HorizontalAlignment = HorizontalAlignment.Left - }; - - button.Click += (sender, e) => - { - MessageBox.Show("设置已保存!", "插件模板", MessageBoxButton.OK, MessageBoxImage.Information); - }; - - panel.Children.Add(button); - - // 设置控件内容 - Content = panel; - } - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/SoftwareLauncher.cs b/Ink Canvas/Helpers/SoftwareLauncher.cs index 395eadf3..69109e84 100644 --- a/Ink Canvas/Helpers/SoftwareLauncher.cs +++ b/Ink Canvas/Helpers/SoftwareLauncher.cs @@ -49,16 +49,13 @@ namespace Ink_Canvas.Helpers if (string.IsNullOrWhiteSpace(softwareName)) return null; - // 64 位进程默认只枚举 64 位注册表视图;32 位希沃常写在 WOW6432Node 下,需一并扫描。 - string[] uninstallRoots = + // 须用 OpenBaseKey + RegistryView 显式指定视图:Registry.LocalMachine.OpenSubKey 跟随进程位数, + // 32 位进程下无法靠拼接 WOW6432Node 路径进入 64 位视图,会找不到 64 位安装的展台。 + const string uninstallSubKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"; + foreach (RegistryView view in new[] { RegistryView.Registry64, RegistryView.Registry32 }) { - @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", - @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall", - }; - - foreach (string root in uninstallRoots) - { - using (RegistryKey key = Registry.LocalMachine.OpenSubKey(root)) + using (RegistryKey baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view)) + using (RegistryKey key = baseKey.OpenSubKey(uninstallSubKey)) { if (key == null) continue; string found = FindInUninstallKey(key, softwareName); diff --git a/Ink Canvas/Helpers/TimeMachine.cs b/Ink Canvas/Helpers/TimeMachine.cs index 58b9c546..1d86caf7 100644 --- a/Ink Canvas/Helpers/TimeMachine.cs +++ b/Ink Canvas/Helpers/TimeMachine.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Windows; using System.Windows.Ink; @@ -100,8 +100,8 @@ namespace Ink_Canvas.Helpers var item = _currentStrokeHistory[_currentIndex]; item.StrokeHasBeenCleared = !item.StrokeHasBeenCleared; _currentIndex--; - OnUndoStateChanged?.Invoke(_currentIndex > -1); - OnRedoStateChanged?.Invoke(_currentStrokeHistory.Count - _currentIndex - 1 > 0); + OnUndoStateChanged?.Invoke(CanUndo); + OnRedoStateChanged?.Invoke(CanRedo); return item; } @@ -137,9 +137,15 @@ namespace Ink_Canvas.Helpers } private void NotifyUndoRedoState() { - OnUndoStateChanged?.Invoke(_currentIndex > -1); - OnRedoStateChanged?.Invoke(_currentStrokeHistory.Count - _currentIndex - 1 > 0); + OnUndoStateChanged?.Invoke(CanUndo); + OnRedoStateChanged?.Invoke(CanRedo); } + + /// 当前历史是否允许撤销。 + public bool CanUndo => _currentIndex > -1; + + /// 当前历史是否允许重做。 + public bool CanRedo => _currentStrokeHistory.Count > 0 && _currentStrokeHistory.Count - _currentIndex - 1 > 0; } public class TimeMachineHistory diff --git a/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs b/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs index 94a17dcf..ff6c06eb 100644 --- a/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs +++ b/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; using System.Threading.Tasks; using System.Windows; using System.Windows.Ink; @@ -19,6 +18,9 @@ namespace Ink_Canvas.Helpers /// internal static class WinRtHandwritingRecognizer { + private static WinRtInk.InkRecognizer _preferredHandwritingRecognizer; + private static bool _preferredHandwritingRecognizerResolved; + private static void LogHandwriting(string message, LogHelper.LogType logType = LogHelper.LogType.Info) { LogHelper.WriteLogToFile("[手写体] " + message, logType); @@ -38,7 +40,9 @@ namespace Ink_Canvas.Helpers { try { - await RecognizeHandwritingAsync(new StrokeCollection()).ConfigureAwait(true); + await RecognizeHandwritingAsync( + WinRtInkShapeRecognizer.CreateMinimalWarmupStrokeCollection(), + verboseTrace: false).ConfigureAwait(true); } catch { @@ -57,15 +61,21 @@ namespace Ink_Canvas.Helpers /// 再对每一分词用 GetTextCandidates(与当前 SDK 中部分版本的 /// 未暴露笔画映射的局限兼容)。 /// - public static async Task RecognizeHandwritingAsync(StrokeCollection strokes) + /// 为 false 时跳过详细识别日志(用于 等)。 + public static async Task RecognizeHandwritingAsync( + StrokeCollection strokes, + bool verboseTrace = true) { if (!IsApiAvailable || strokes == null || strokes.Count == 0) return HandwritingRecognitionResult.Empty; - var traceRecognition = strokes.Count > 0; + var traceRecognition = verboseTrace; try { + var recognizer = new WinRtInk.InkRecognizerContainer(); + TryApplyPreferredHandwritingRecognizer(recognizer, traceRecognition); + var analyzer = new WinAnalysis.InkAnalyzer(); var idToWpf = new Dictionary(); @@ -92,19 +102,21 @@ namespace Ink_Canvas.Helpers LogHandwriting( "识别:AnalyzeAsync 未得到 Updated,Status=" + (analysisResult == null ? "null" : analysisResult.Status.ToString()) + - ",有效笔画数=" + idToWpf.Count); - return HandwritingRecognitionResult.Empty; + ",有效笔画数=" + idToWpf.Count + + ",尝试整批 RecognizeAsync 回退。"); + return await RecognizeHandwritingWholeInkAsync(strokes, traceRecognition).ConfigureAwait(true); } var wordNodes = analyzer.AnalysisRoot?.FindNodes(WinAnalysis.InkAnalysisNodeKind.InkWord); if (wordNodes == null || wordNodes.Count == 0) { if (traceRecognition) - LogHandwriting("识别:未找到 InkWord 节点(可能被判为绘图或非书写),有效笔画数=" + idToWpf.Count); - return HandwritingRecognitionResult.Empty; + LogHandwriting( + "识别:未找到 InkWord 节点(墨迹分析常将非横平笔划判为绘图),有效笔画数=" + idToWpf.Count + + ",改用整批 RecognizeAsync 回退。"); + return await RecognizeHandwritingWholeInkAsync(strokes, traceRecognition).ConfigureAwait(true); } - var recognizer = new WinRtInk.InkRecognizerContainer(); var segments = new List(); foreach (var node in wordNodes) @@ -232,6 +244,243 @@ namespace Ink_Canvas.Helpers } } + private static void TryApplyPreferredHandwritingRecognizer( + WinRtInk.InkRecognizerContainer container, + bool logDetail) + { + if (container == null) + return; + try + { + if (!_preferredHandwritingRecognizerResolved) + { + _preferredHandwritingRecognizerResolved = true; + var all = container.GetRecognizers(); + _preferredHandwritingRecognizer = SelectBestInkRecognizer(all); + if (logDetail) + { + if (_preferredHandwritingRecognizer != null) + LogHandwriting("识别器:已选用 \"" + _preferredHandwritingRecognizer.Name + "\"。"); + else if (all != null && all.Count > 0) + LogHandwriting("识别器:未匹配到与 UI/区域语言对应的引擎,使用系统默认(共 " + all.Count + " 个)。"); + } + } + + if (_preferredHandwritingRecognizer != null) + container.SetDefaultRecognizer(_preferredHandwritingRecognizer); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile("[手写体] 设置默认手写识别器失败: " + ex.Message, LogHelper.LogType.Warning); + } + } + + private static WinRtInk.InkRecognizer SelectBestInkRecognizer( + IReadOnlyList list) + { + if (list == null || list.Count == 0) + return null; + + var culture = PrimaryHandwritingCulture(); + var lang = (culture?.TwoLetterISOLanguageName ?? string.Empty).ToLowerInvariant(); + var name = culture?.Name ?? string.Empty; + + bool wantZhHans = lang == "zh" && + (name.IndexOf("hans", StringComparison.OrdinalIgnoreCase) >= 0 || + name.Equals("zh-cn", StringComparison.OrdinalIgnoreCase) || + name.Equals("zh-sg", StringComparison.OrdinalIgnoreCase) || + (name.IndexOf("hant", StringComparison.OrdinalIgnoreCase) < 0 && + !name.Equals("zh-tw", StringComparison.OrdinalIgnoreCase) && + !name.Equals("zh-hk", StringComparison.OrdinalIgnoreCase) && + !name.Equals("zh-mo", StringComparison.OrdinalIgnoreCase))); + + bool wantZhHant = lang == "zh" && + (name.IndexOf("hant", StringComparison.OrdinalIgnoreCase) >= 0 || + name.Equals("zh-tw", StringComparison.OrdinalIgnoreCase) || + name.Equals("zh-hk", StringComparison.OrdinalIgnoreCase) || + name.Equals("zh-mo", StringComparison.OrdinalIgnoreCase)); + + WinRtInk.InkRecognizer Pick(Func match) + { + foreach (var r in list) + { + var n = r?.Name; + if (string.IsNullOrEmpty(n)) + continue; + if (match(n)) + return r; + } + + return null; + } + + if (wantZhHans) + { + var r = Pick(n => + n.IndexOf("简体", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("簡體", StringComparison.OrdinalIgnoreCase) >= 0 || + (n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 && + (n.IndexOf("简体", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("簡體", StringComparison.OrdinalIgnoreCase) >= 0)) || + (n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0 && + (n.IndexOf("Simplified", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Hans", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("PRC", StringComparison.OrdinalIgnoreCase) >= 0))); + if (r != null) + return r; + r = Pick(n => + n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0); + if (r != null) + return r; + } + else if (wantZhHant) + { + var r = Pick(n => + n.IndexOf("繁体", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("繁體", StringComparison.OrdinalIgnoreCase) >= 0 || + (n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 && + (n.IndexOf("繁体", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("繁體", StringComparison.OrdinalIgnoreCase) >= 0)) || + (n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0 && + (n.IndexOf("Traditional", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Hant", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Taiwan", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Hong Kong", StringComparison.OrdinalIgnoreCase) >= 0))); + if (r != null) + return r; + r = Pick(n => + n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0); + if (r != null) + return r; + } + else if (lang == "ja") + { + var r = Pick(n => + n.IndexOf("Japanese", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("日本語", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("日语", StringComparison.OrdinalIgnoreCase) >= 0); + if (r != null) + return r; + } + else if (lang == "en") + { + var r = Pick(n => n.IndexOf("English", StringComparison.OrdinalIgnoreCase) >= 0); + if (r != null) + return r; + } + + if (lang == "zh") + { + var r = Pick(n => + n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0); + if (r != null) + return r; + } + + return null; + } + + private static CultureInfo PrimaryHandwritingCulture() + { + var ui = CultureInfo.CurrentUICulture; + var ct = CultureInfo.CurrentCulture; + if (string.Equals(ui.TwoLetterISOLanguageName, "zh", StringComparison.OrdinalIgnoreCase)) + return ui; + if (string.Equals(ct.TwoLetterISOLanguageName, "zh", StringComparison.OrdinalIgnoreCase)) + return ct; + return ui; + } + + private static async Task RecognizeHandwritingWholeInkAsync( + StrokeCollection strokes, + bool traceRecognition) + { + if (strokes == null || strokes.Count == 0) + return HandwritingRecognitionResult.Empty; + + var container = new WinRtInk.InkStrokeContainer(); + foreach (Stroke s in strokes) + { + var ink = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(s); + if (ink != null) + container.AddStroke(ink); + } + + var winStrokes = container.GetStrokes(); + if (winStrokes == null || winStrokes.Count == 0) + { + if (traceRecognition) + LogHandwriting("整批回退:无有效 WinRT 笔画。"); + return HandwritingRecognitionResult.Empty; + } + + var reco = new WinRtInk.InkRecognizerContainer(); + TryApplyPreferredHandwritingRecognizer(reco, false); + + IReadOnlyList rr; + try + { + rr = await reco + .RecognizeAsync(container, WinRtInk.InkRecognitionTarget.All) + .AsTask() + .ConfigureAwait(true); + } + catch (Exception ex) + { + if (traceRecognition) + LogHandwriting("整批回退:RecognizeAsync 异常:" + ex.Message); + return HandwritingRecognitionResult.Empty; + } + + if (rr == null || rr.Count == 0 || rr[0] == null) + { + if (traceRecognition) + LogHandwriting("整批回退:RecognizeAsync 无结果。"); + return HandwritingRecognitionResult.Empty; + } + + var cands = rr[0].GetTextCandidates(); + var primary = (cands != null && cands.Count > 0) ? cands[0] : string.Empty; + if (string.IsNullOrWhiteSpace(primary)) + { + if (traceRecognition) + LogHandwriting("整批回退:候选文本为空。"); + return HandwritingRecognitionResult.Empty; + } + + var merged = new List(); + if (cands != null) + { + foreach (var c in cands) + { + if (!string.IsNullOrEmpty(c) && !merged.Contains(c)) + merged.Add(c); + } + } + + var bounds = UnionStrokeBounds(strokes); + var group = new List(); + foreach (Stroke s in strokes) + group.Add(s); + + var seg = new HandwritingWordSegment(primary, merged, bounds, group); + return new HandwritingRecognitionResult(new List { seg }); + } + + private static Rect UnionStrokeBounds(StrokeCollection strokes) + { + if (strokes == null || strokes.Count == 0) + return Rect.Empty; + + var r = strokes[0].GetBounds(); + for (var i = 1; i < strokes.Count; i++) + r = Rect.Union(r, strokes[i].GetBounds()); + return r; + } + private const string DefaultHandwritingFontFamilyList = "Ink Free,KaiTi,Segoe Script"; /// diff --git a/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs b/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs index bfd0618f..c39a3621 100644 --- a/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs +++ b/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs @@ -28,7 +28,8 @@ namespace Ink_Canvas.Helpers { try { - await RecognizeShapeAsync(new StrokeCollection()); + // 空 StrokeCollection 在 RecognizeShapeAsync 入口会直接返回,无法预热 WinRT InkAnalyzer。 + await RecognizeShapeAsync(CreateMinimalWarmupStrokeCollection()).ConfigureAwait(true); } catch { @@ -99,6 +100,23 @@ namespace Ink_Canvas.Helpers } } + /// + /// 极短合成笔画,供 等场景走完整 WinRT 转换与分析管线(空集合在入口处会被直接返回)。 + /// + internal static StrokeCollection CreateMinimalWarmupStrokeCollection() + { + var da = new DrawingAttributes { Color = Colors.Black, Width = 2, Height = 2 }; + var pts = new StylusPointCollection + { + new StylusPoint(8, 8), + new StylusPoint(14, 10), + new StylusPoint(20, 8), + }; + var col = new StrokeCollection(); + col.Add(new Stroke(pts, da)); + return col; + } + /// 供 WinRT 手写等模块复用:将 WPF 转为 WinRT internal static global::Windows.UI.Input.Inking.InkStroke CreateInkStrokeFromWpf(Stroke stroke) { diff --git a/Ink Canvas/Helpers/WindowsNotificationHelper.cs b/Ink Canvas/Helpers/WindowsNotificationHelper.cs index 13f15c82..652732eb 100644 --- a/Ink Canvas/Helpers/WindowsNotificationHelper.cs +++ b/Ink Canvas/Helpers/WindowsNotificationHelper.cs @@ -1,4 +1,4 @@ -using Hardcodet.Wpf.TaskbarNotification; +using H.NotifyIcon; using Microsoft.Toolkit.Uwp.Notifications; using System; using System.Windows; @@ -40,10 +40,9 @@ namespace Ink_Canvas.Helpers taskbar.Visibility = Visibility.Visible; - taskbar.ShowBalloonTip( + taskbar.ShowNotification( "InkCanvasForClass CE", - $"发现新版本!:{version}", - BalloonIcon.Info); + $"发现新版本!:{version}"); } catch { diff --git a/Ink Canvas/InkCanvasForClass.csproj b/Ink Canvas/InkCanvasForClass.csproj index 88836384..abe1f3f3 100644 --- a/Ink Canvas/InkCanvasForClass.csproj +++ b/Ink Canvas/InkCanvasForClass.csproj @@ -114,6 +114,7 @@ + @@ -135,12 +136,13 @@ all - + + @@ -563,14 +565,7 @@ - - - - - - Ink_Canvas.Properties.Strings.enUS.xml - - + diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index 698fd6da..cf129f5f 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -247,21 +247,6 @@ - - - private void ProcessElementsAfterRestore(List elements) { - if (elements == null || elements.Count == 0) return; + if (elements == null || elements.Count == 0) + { + SyncPdfPageSidebarWithCanvas(); + return; + } // 使用低优先级异步处理,让 UI 先响应,图片位置和事件绑定稍后完成 Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() => @@ -310,7 +316,19 @@ namespace Ink_Canvas } BindElementEvents(media); } + else if (element is PdfEmbeddedView pdf) + { + double left = InkCanvas.GetLeft(pdf); + double top = InkCanvas.GetTop(pdf); + if (double.IsNaN(left) || double.IsNaN(top)) + { + CenterAndScaleElement(pdf); + } + BindElementEvents(pdf); + } } + + SyncPdfPageSidebarWithCanvas(); })); } diff --git a/Ink Canvas/MainWindow_cs/MW_Colors.cs b/Ink Canvas/MainWindow_cs/MW_Colors.cs index 6eb8703e..133d23c9 100644 --- a/Ink Canvas/MainWindow_cs/MW_Colors.cs +++ b/Ink Canvas/MainWindow_cs/MW_Colors.cs @@ -52,6 +52,7 @@ namespace Ink_Canvas AnimationsHelper.HideWithSlideAndFade(TwoFingerGestureBorder); AnimationsHelper.HideWithSlideAndFade(BoardTwoFingerGestureBorder); EnableTwoFingerGestureBorder.Visibility = Visibility.Collapsed; + SyncPdfPageSidebarWithCanvas(); } BtnHideInkCanvas_Click(BtnHideInkCanvas, null); diff --git a/Ink Canvas/MainWindow_cs/MW_ElementsControls.cs b/Ink Canvas/MainWindow_cs/MW_ElementsControls.cs index 281c3f27..e91bcb73 100644 --- a/Ink Canvas/MainWindow_cs/MW_ElementsControls.cs +++ b/Ink Canvas/MainWindow_cs/MW_ElementsControls.cs @@ -1,3 +1,4 @@ +using Ink_Canvas.Controls; using Ink_Canvas.Helpers; using Microsoft.Win32; using System; @@ -34,6 +35,14 @@ namespace Ink_Canvas /// private Point dragStartPoint; + /// 页码侧栏当前订阅 的 PDF 视图。 + private PdfEmbeddedView _pdfPageSidebarEventSource; + + private bool _pdfSidebarPositionRefreshPending; + + /// 为 true 时,下一次成功算出 PDF 边界后的侧栏定位使用宿主 Visual 变换(仅用于刚插入/恢复 PDF 的首帧对齐)。 + private bool _pdfSidebarNextPositionUseHostTransform; + #region Image /// /// 处理图片插入按钮点击事件 @@ -56,48 +65,52 @@ namespace Ink_Canvas private async void BtnImageInsert_Click(object sender, RoutedEventArgs e) { OpenFileDialog openFileDialog = new OpenFileDialog(); - openFileDialog.Filter = "Image files (*.jpg; *.jpeg; *.png; *.bmp)|*.jpg;*.jpeg;*.png;*.bmp"; + openFileDialog.Filter = "图片与 PDF|*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.pdf|图片文件|*.jpg;*.jpeg;*.png;*.bmp;*.gif|PDF|*.pdf"; if (openFileDialog.ShowDialog() == true) { string filePath = openFileDialog.FileName; - Image image = await CreateAndCompressImageAsync(filePath); + FrameworkElement element = await CreateAndCompressImageAsync(filePath); - if (image != null) + if (element != null) { string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff"); - image.Name = timestamp; + element.Name = timestamp; // 设置图片属性,避免被InkCanvas选择系统处理 - image.IsHitTestVisible = true; - image.Focusable = false; + element.IsHitTestVisible = true; + element.Focusable = false; // 初始化InkCanvas选择设置 InitializeInkCanvasSelectionSettings(); // 先添加到画布 - inkCanvas.Children.Add(image); + inkCanvas.Children.Add(element); // 等待图片加载完成后再进行后续处理 - image.Loaded += (s, args) => + element.Loaded += (s, args) => { Dispatcher.BeginInvoke(new Action(() => { // 初始化TransformGroup - InitializeElementTransform(image); + InitializeElementTransform(element); // 居中缩放 - CenterAndScaleElement(image); + CenterAndScaleElement(element); // 最后绑定事件处理器 - BindElementEvents(image); + BindElementEvents(element); - LogHelper.WriteLogToFile($"图片插入完成: {image.Name}"); + if (element is PdfEmbeddedView) + _pdfSidebarNextPositionUseHostTransform = true; + SyncPdfPageSidebarWithCanvas(); + + LogHelper.WriteLogToFile($"图片插入完成: {element.Name}"); }), DispatcherPriority.Loaded); }; - timeMachine.CommitElementInsertHistory(image); + timeMachine.CommitElementInsertHistory(element); // 插入图片后切换到选择模式并刷新浮动栏高光显示 SetCurrentToolMode(InkCanvasEditingMode.Select); @@ -270,13 +283,13 @@ namespace Ink_Canvas ApplyMouseDragTransform(element, currentPoint, dragStartPoint); // 如果是图片元素,更新工具栏位置 - if (element is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible) + if (IsBitmapLikeCanvasElement(element) && BorderImageSelectionControl?.Visibility == Visibility.Visible) { UpdateImageSelectionToolbarPosition(element); } // 如果是图片元素,更新选择点位置 - if (element is Image && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible) + if (IsBitmapLikeCanvasElement(element) && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible) { UpdateImageResizeHandlesPosition(GetElementActualBounds(element)); } @@ -306,13 +319,13 @@ namespace Ink_Canvas ApplyWheelScaleTransform(element, e); // 如果是图片元素,更新工具栏位置 - if (element is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible) + if (IsBitmapLikeCanvasElement(element) && BorderImageSelectionControl?.Visibility == Visibility.Visible) { UpdateImageSelectionToolbarPosition(element); } // 如果是图片元素,更新选择点位置 - if (element is Image && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible) + if (IsBitmapLikeCanvasElement(element) && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible) { UpdateImageResizeHandlesPosition(GetElementActualBounds(element)); } @@ -396,13 +409,13 @@ namespace Ink_Canvas ApplyTouchManipulationTransform(element, e); // 如果是图片元素,更新工具栏位置 - if (element is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible) + if (IsBitmapLikeCanvasElement(element) && BorderImageSelectionControl?.Visibility == Visibility.Visible) { UpdateImageSelectionToolbarPosition(element); } // 如果是图片元素,更新选择点位置 - if (element is Image && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible) + if (IsBitmapLikeCanvasElement(element) && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible) { UpdateImageResizeHandlesPosition(GetElementActualBounds(element)); } @@ -523,12 +536,12 @@ namespace Ink_Canvas currentSelectedElement = element; // 根据元素类型显示不同的选择工具栏 - if (element is Image) + if (IsBitmapLikeCanvasElement(element)) { // 显示图片选择工具栏并设置位置 if (BorderImageSelectionControl != null) { - // 计算工具栏位置 + // 计算工具栏位置(内部会同步 PDF 右侧栏位置) UpdateImageSelectionToolbarPosition(element); BorderImageSelectionControl.Visibility = Visibility.Visible; } @@ -568,6 +581,8 @@ namespace Ink_Canvas // 保持选择模式,这样用户可以直接点击墨迹来选择 inkCanvas.EditingMode = InkCanvasEditingMode.Select; } + + SyncPdfPageSidebarWithCanvas(); } /// @@ -607,6 +622,8 @@ namespace Ink_Canvas { inkCanvas.EditingMode = InkCanvasEditingMode.Select; } + + SyncPdfPageSidebarWithCanvas(); } /// @@ -914,15 +931,24 @@ namespace Ink_Canvas /// - 否则使用原始尺寸 /// - 返回创建的Image对象 /// - private async Task CreateAndCompressImageAsync(string filePath) + /// 与图片选择工具栏、缩放控制点联动的画布位图类元素(普通图片或多页 PDF 嵌入)。 + private static bool IsBitmapLikeCanvasElement(FrameworkElement fe) { + return fe is Image || fe is PdfEmbeddedView; + } + + private async Task CreateAndCompressImageAsync(string filePath) + { + string fileExtension = Path.GetExtension(filePath); + if (string.Equals(fileExtension, ".pdf", StringComparison.OrdinalIgnoreCase)) + return await CreateAndCompressImageFromPdfAsync(filePath); + string savePath = Path.Combine(Settings.Automation.AutoSavedStrokesLocation, "File Dependency"); if (!Directory.Exists(savePath)) { Directory.CreateDirectory(savePath); } - string fileExtension = Path.GetExtension(filePath); string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff"); string newFilePath = Path.Combine(savePath, timestamp + fileExtension); @@ -965,6 +991,84 @@ namespace Ink_Canvas return image; }); } + + /// + /// 插入完整 PDF:嵌入控件内可翻页,右下角显示页码(类似希沃白板交互)。 + /// + private async Task CreateAndCompressImageFromPdfAsync(string filePath) + { + try + { + string savePath = Path.Combine(Settings.Automation.AutoSavedStrokesLocation, "File Dependency"); + if (!Directory.Exists(savePath)) + Directory.CreateDirectory(savePath); + + string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff"); + string newFilePath = Path.Combine(savePath, timestamp + ".pdf"); + await Task.Run(() => File.Copy(filePath, newFilePath, true)); + + uint pageCount = await PdfWinRtHelper.GetPageCountAsync(newFilePath); + if (pageCount == 0) + { + ShowNotification("无法打开 PDF(可能已加密、损坏或不支持)。"); + return null; + } + + bool compress = isLoaded && Settings.Canvas.IsCompressPicturesUploaded; + var view = new PdfEmbeddedView(); + await view.InitializeAsync(newFilePath, pageCount, compress); + view.Tag = filePath; + return view; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"插入 PDF 失败: {ex.Message}", LogHelper.LogType.Error); + ShowNotification($"插入 PDF 失败: {ex.Message}"); + return null; + } + } + + /// 从保存的 恢复 PDF(与打开墨迹时的图片恢复流程一致,不单独写入时间轴)。 + private async Task RestorePdfFromElementInfoAsync(CanvasElementInfo info) + { + if (info == null || inkCanvas == null) return; + if (!string.Equals(info.Type, "Pdf", StringComparison.OrdinalIgnoreCase)) return; + if (string.IsNullOrEmpty(info.SourcePath) || !File.Exists(info.SourcePath)) return; + + try + { + uint pageCount = await PdfWinRtHelper.GetPageCountAsync(info.SourcePath); + if (pageCount == 0) return; + + bool compress = isLoaded && Settings.Canvas.IsCompressPicturesUploaded; + uint initial = 0; + if (info.PdfCurrentPage.HasValue) + initial = (uint)Math.Max(0, Math.Min(info.PdfCurrentPage.Value, (int)pageCount - 1)); + + var view = new PdfEmbeddedView + { + Name = "pdf_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff") + }; + await view.InitializeAsync(info.SourcePath, pageCount, compress, initial); + + if (info.Width > 0) view.Width = info.Width; + if (info.Height > 0) view.Height = info.Height; + + InkCanvas.SetLeft(view, info.Left); + InkCanvas.SetTop(view, info.Top); + + InitializeElementTransform(view); + BindElementEvents(view); + inkCanvas.Children.Add(view); + _pdfSidebarNextPositionUseHostTransform = true; + SyncPdfPageSidebarWithCanvas(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"从 .elements.json 恢复 PDF 失败: {ex.Message}", LogHelper.LogType.Error); + } + } + #endregion #region Media @@ -1453,6 +1557,10 @@ namespace Ink_Canvas // 设置工具栏位置 BorderImageSelectionControl.Margin = new Thickness(toolbarLeft, toolbarTop, 0, 0); + + var pdfTarget = GetPdfSidebarTargetElement(); + if (pdfTarget != null && BorderPdfPageSidebar != null && BorderPdfPageSidebar.Visibility == Visibility.Visible) + UpdatePdfPageSidebarPosition(pdfTarget); } catch (Exception ex) { @@ -1460,6 +1568,245 @@ namespace Ink_Canvas } } + private const double PdfPageSidebarGap = 10; + + /// + /// 侧栏绑定的 PDF:若当前选中的是 PDF 则用该项;否则用画布上最后一个 PdfEmbeddedView。 + /// + private PdfEmbeddedView GetPdfSidebarTargetElement() + { + if (inkCanvas == null) return null; + var pdfs = inkCanvas.Children.OfType().ToList(); + if (pdfs.Count == 0) return null; + if (currentSelectedElement is PdfEmbeddedView sel && pdfs.Contains(sel)) + return sel; + return pdfs[pdfs.Count - 1]; + } + + private void AttachPdfPageSidebarEvents(PdfEmbeddedView pdf) + { + if (pdf == null || _pdfPageSidebarEventSource == pdf) return; + DetachPdfPageSidebarEvents(); + _pdfPageSidebarEventSource = pdf; + _pdfPageSidebarEventSource.PageNavigationStateChanged += SelectedPdf_PageNavigationStateChanged; + _pdfPageSidebarEventSource.LayoutUpdated += OnPdfSidebarTargetLayoutUpdated; + } + + private void DetachPdfPageSidebarEvents() + { + if (_pdfPageSidebarEventSource != null) + { + _pdfPageSidebarEventSource.PageNavigationStateChanged -= SelectedPdf_PageNavigationStateChanged; + _pdfPageSidebarEventSource.LayoutUpdated -= OnPdfSidebarTargetLayoutUpdated; + _pdfPageSidebarEventSource = null; + } + } + + private void OnPdfSidebarTargetLayoutUpdated(object sender, EventArgs e) + { + if (BorderPdfPageSidebar?.Visibility != Visibility.Visible || inkCanvas == null) return; + if (!(sender is PdfEmbeddedView p) || !ReferenceEquals(_pdfPageSidebarEventSource, p)) return; + if (!inkCanvas.Children.Contains(p)) + SyncPdfPageSidebarWithCanvas(); + else + RequestPdfSidebarPositionRefresh(); + } + + /// 在下一帧合并更新侧栏位置(初始布局、翻页改尺寸后 ActualWidth 仍为 0 时尤其需要)。 + private void RequestPdfSidebarPositionRefresh() + { + if (_pdfSidebarPositionRefreshPending) return; + _pdfSidebarPositionRefreshPending = true; + Dispatcher.BeginInvoke(new Action(() => + { + _pdfSidebarPositionRefreshPending = false; + try + { + var t = GetPdfSidebarTargetElement(); + if (t == null || BorderPdfPageSidebar?.Visibility != Visibility.Visible) + { + SyncPdfPageSidebarWithCanvas(); + return; + } + + if (!inkCanvas.Children.Contains(t)) + { + SyncPdfPageSidebarWithCanvas(); + return; + } + + t.UpdateLayout(); + inkCanvas.UpdateLayout(); + UpdatePdfPageSidebarPosition(t); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"PDF 侧栏延迟定位失败: {ex.Message}", LogHelper.LogType.Warning); + } + }), DispatcherPriority.Render); + } + + /// + /// 画布上存在 PDF 时始终显示右侧页码栏并跟随目标 PDF;无任何 PDF 时隐藏。 + /// + private void SyncPdfPageSidebarWithCanvas() + { + if (BorderPdfPageSidebar == null || inkCanvas == null) return; + + // 屏幕模式(已退出白板/黑板)下不显示侧栏,避免画布仍含 PDF 时栏残留在桌面上 + if (currentMode == 0) + { + DetachPdfPageSidebarEvents(); + BorderPdfPageSidebar.Visibility = Visibility.Collapsed; + ResetPdfSidebarToIdle(); + return; + } + + var pdf = GetPdfSidebarTargetElement(); + if (pdf == null) + { + DetachPdfPageSidebarEvents(); + BorderPdfPageSidebar.Visibility = Visibility.Collapsed; + ResetPdfSidebarToIdle(); + return; + } + + AttachPdfPageSidebarEvents(pdf); + BorderPdfPageSidebar.Visibility = Visibility.Visible; + UpdatePdfSidebarFromPdf(pdf); + pdf.UpdateLayout(); + inkCanvas.UpdateLayout(); + UpdatePdfPageSidebarPosition(pdf); + RequestPdfSidebarPositionRefresh(); + Dispatcher.BeginInvoke(new Action(() => + { + var t = GetPdfSidebarTargetElement(); + if (t != null && BorderPdfPageSidebar?.Visibility == Visibility.Visible && inkCanvas.Children.Contains(t)) + UpdatePdfPageSidebarPosition(t); + }), DispatcherPriority.ContextIdle); + } + + /// + /// 将 PDF 专用页码栏贴在当前 PDF 右侧。常态与早期实现一致:画布坐标 + Measure(Width, ∞);仅在 为 true 时用宿主 Visual 变换对齐首帧。 + /// + private void UpdatePdfPageSidebarPosition(FrameworkElement element) + { + try + { + if (BorderPdfPageSidebar == null || inkCanvas == null || !(element is PdfEmbeddedView pdfEl)) + return; + + if (!inkCanvas.Children.Contains(pdfEl)) + { + SyncPdfPageSidebarWithCanvas(); + return; + } + + bool wantHostOnce = _pdfSidebarNextPositionUseHostTransform; + + // 插入首帧:先布局再取界,便于 Transform 与墨迹一致;常态与最初 PDF 侧栏实现一致,不在此强制 UpdateLayout。 + if (wantHostOnce) + pdfEl.UpdateLayout(); + + Rect b = GetElementActualBounds(pdfEl); + if (b.Width <= 0 || b.Height <= 0 || double.IsNaN(b.Width) || double.IsNaN(b.Height)) + { + Dispatcher.BeginInvoke(new Action(() => + { + var t = GetPdfSidebarTargetElement(); + if (t is PdfEmbeddedView pe && inkCanvas.Children.Contains(pe) && BorderPdfPageSidebar?.Visibility == Visibility.Visible) + UpdatePdfPageSidebarPosition(pe); + }), DispatcherPriority.Loaded); + return; + } + + Visual sidebarHost = VisualTreeHelper.GetParent(BorderPdfPageSidebar) as Visual; + double left = 0, top = 0, maxLeft = 0, maxTop = 0; + bool hostOk = false; + + if (wantHostOnce && sidebarHost != null) + { + BorderPdfPageSidebar.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + double sidebarW = BorderPdfPageSidebar.DesiredSize.Width; + double sidebarH = BorderPdfPageSidebar.DesiredSize.Height; + if (sidebarW <= 0) + sidebarW = BorderPdfPageSidebar.Width; + if (sidebarH <= 0) + sidebarH = BorderPdfPageSidebar.ActualHeight; + if (sidebarH <= 0) + sidebarH = 220; + + try + { + GeneralTransform inkToHost = inkCanvas.TransformToVisual(sidebarHost); + Point tl = inkToHost.Transform(new Point(b.Left, b.Top)); + Point br = inkToHost.Transform(new Point(b.Right, b.Bottom)); + double rightX = Math.Max(tl.X, br.X); + double midY = (tl.Y + br.Y) * 0.5; + left = rightX + PdfPageSidebarGap; + top = midY - sidebarH * 0.5; + + var feHost = sidebarHost as FrameworkElement; + double hostW = feHost != null && feHost.ActualWidth > 0 ? feHost.ActualWidth : inkCanvas.ActualWidth; + double hostH = feHost != null && feHost.ActualHeight > 0 ? feHost.ActualHeight : inkCanvas.ActualHeight; + maxLeft = Math.Max(0, hostW - sidebarW); + maxTop = Math.Max(0, hostH - sidebarH); + + if (left > maxLeft) + { + double leftEdge = Math.Min(tl.X, br.X); + double leftAlt = leftEdge - PdfPageSidebarGap - sidebarW; + if (leftAlt >= 0) + left = leftAlt; + } + + hostOk = true; + } + catch + { + hostOk = false; + } + } + + if (!hostOk) + { + // 与 ea74592「PDF 侧栏」初版一致:固定宽度测量竖向所需高度,再按墨迹边界与 inkCanvas 尺寸夹紧。 + BorderPdfPageSidebar.Measure(new Size(BorderPdfPageSidebar.Width, double.PositiveInfinity)); + double sidebarW = BorderPdfPageSidebar.DesiredSize.Width; + double sidebarH = BorderPdfPageSidebar.DesiredSize.Height; + if (sidebarW <= 0) + sidebarW = BorderPdfPageSidebar.Width; + if (sidebarH <= 0) + sidebarH = BorderPdfPageSidebar.ActualHeight; + if (sidebarH <= 0) + sidebarH = 220; + + left = b.Right + PdfPageSidebarGap; + top = b.Top + (b.Height * 0.5) - (sidebarH * 0.5); + maxLeft = Math.Max(0, inkCanvas.ActualWidth - sidebarW); + maxTop = Math.Max(0, inkCanvas.ActualHeight - sidebarH); + if (left > maxLeft) + { + double leftAlt = b.Left - PdfPageSidebarGap - sidebarW; + if (leftAlt >= 0) + left = leftAlt; + } + } + + if (wantHostOnce) + _pdfSidebarNextPositionUseHostTransform = false; + + left = Math.Max(0, Math.Min(left, maxLeft)); + top = Math.Max(0, Math.Min(top, maxTop)); + + BorderPdfPageSidebar.Margin = new Thickness(left, top, 0, 0); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"更新 PDF 右侧页码栏位置失败: {ex.Message}", LogHelper.LogType.Error); + } + } + /// /// 获取元素的实际边界(考虑变换) /// @@ -1644,7 +1991,7 @@ namespace Ink_Canvas ApplyRotateTransform(currentSelectedElement, -45); // 更新工具栏位置 - if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible) + if (IsBitmapLikeCanvasElement(currentSelectedElement) && BorderImageSelectionControl?.Visibility == Visibility.Visible) { UpdateImageSelectionToolbarPosition(currentSelectedElement); } @@ -1678,7 +2025,7 @@ namespace Ink_Canvas ApplyRotateTransform(currentSelectedElement, 45); // 更新工具栏位置 - if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible) + if (IsBitmapLikeCanvasElement(currentSelectedElement) && BorderImageSelectionControl?.Visibility == Visibility.Visible) { UpdateImageSelectionToolbarPosition(currentSelectedElement); } @@ -1714,7 +2061,7 @@ namespace Ink_Canvas ApplyScaleTransform(currentSelectedElement, 0.9, elementCenter); // 更新工具栏位置 - if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible) + if (IsBitmapLikeCanvasElement(currentSelectedElement) && BorderImageSelectionControl?.Visibility == Visibility.Visible) { UpdateImageSelectionToolbarPosition(currentSelectedElement); } @@ -1750,7 +2097,7 @@ namespace Ink_Canvas ApplyScaleTransform(currentSelectedElement, 1.1, elementCenter); // 更新工具栏位置 - if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible) + if (IsBitmapLikeCanvasElement(currentSelectedElement) && BorderImageSelectionControl?.Visibility == Visibility.Visible) { UpdateImageSelectionToolbarPosition(currentSelectedElement); } @@ -1764,19 +2111,110 @@ namespace Ink_Canvas } } + private void ResetPdfSidebarToIdle() + { + if (TextBlockPdfSidebarPageLabel != null) + { + TextBlockPdfSidebarPageLabel.Text = "— / —"; + TextBlockPdfSidebarPageLabel.Opacity = 0.55; + } + + if (BorderPdfSidebarPagePrev != null) + { + BorderPdfSidebarPagePrev.Opacity = 0.35; + BorderPdfSidebarPagePrev.IsHitTestVisible = false; + } + + if (BorderPdfSidebarPageNext != null) + { + BorderPdfSidebarPageNext.Opacity = 0.35; + BorderPdfSidebarPageNext.IsHitTestVisible = false; + } + } + + private void UpdatePdfSidebarFromPdf(PdfEmbeddedView pdf) + { + if (pdf == null) return; + + if (TextBlockPdfSidebarPageLabel != null) + { + TextBlockPdfSidebarPageLabel.Text = pdf.PageLabelText; + TextBlockPdfSidebarPageLabel.Opacity = 1.0; + } + + bool prevOk = pdf.CanGoPrevious; + bool nextOk = pdf.CanGoNext; + if (BorderPdfSidebarPagePrev != null) + { + BorderPdfSidebarPagePrev.Opacity = prevOk ? 1.0 : 0.35; + BorderPdfSidebarPagePrev.IsHitTestVisible = prevOk; + } + + if (BorderPdfSidebarPageNext != null) + { + BorderPdfSidebarPageNext.Opacity = nextOk ? 1.0 : 0.35; + BorderPdfSidebarPageNext.IsHitTestVisible = nextOk; + } + } + + private void SelectedPdf_PageNavigationStateChanged(object sender, EventArgs e) + { + Dispatcher.BeginInvoke(new Action(() => + { + if (sender is PdfEmbeddedView pdf) + { + if (!inkCanvas.Children.Contains(pdf)) + { + SyncPdfPageSidebarWithCanvas(); + return; + } + + UpdatePdfSidebarFromPdf(pdf); + UpdatePdfPageSidebarPosition(pdf); + RequestPdfSidebarPositionRefresh(); + } + if (currentSelectedElement != null && IsBitmapLikeCanvasElement(currentSelectedElement)) + { + UpdateImageSelectionToolbarPosition(currentSelectedElement); + if (ImageResizeHandlesCanvas?.Visibility == Visibility.Visible) + UpdateImageResizeHandlesPosition(GetElementActualBounds(currentSelectedElement)); + } + }), DispatcherPriority.Loaded); + } + + private async void BorderPdfSidebarPagePrev_MouseUp(object sender, MouseButtonEventArgs e) + { + try + { + var pdf = GetPdfSidebarTargetElement(); + if (pdf != null && pdf.CanGoPrevious) + await pdf.GoToPreviousPageAsync(); + SyncPdfPageSidebarWithCanvas(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"PDF 上一页失败: {ex.Message}", LogHelper.LogType.Error); + } + } + + private async void BorderPdfSidebarPageNext_MouseUp(object sender, MouseButtonEventArgs e) + { + try + { + var pdf = GetPdfSidebarTargetElement(); + if (pdf != null && pdf.CanGoNext) + await pdf.GoToNextPageAsync(); + SyncPdfPageSidebarWithCanvas(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"PDF 下一页失败: {ex.Message}", LogHelper.LogType.Error); + } + } + /// /// 处理图片删除功能 /// - /// 事件发送者 - /// 事件参数 - /// - /// - 检查当前是否有选中元素 - /// - 保存删除前的编辑模式 - /// - 记录删除历史 - /// - 从画布中移除 - /// - 清除选中状态 - /// - 包含异常处理 - /// private void BorderImageDelete_MouseUp(object sender, MouseButtonEventArgs e) { try @@ -1789,16 +2227,19 @@ namespace Ink_Canvas // 记录删除历史 timeMachine.CommitElementRemoveHistory(currentSelectedElement); + var toRemove = currentSelectedElement; // 从画布中移除 - inkCanvas.Children.Remove(currentSelectedElement); + inkCanvas.Children.Remove(toRemove); // 清除选中状态 - UnselectElement(currentSelectedElement); + UnselectElement(toRemove); currentSelectedElement = null; // 恢复到删除前的编辑模式 inkCanvas.EditingMode = previousEditingMode; + SyncPdfPageSidebarWithCanvas(); + LogHelper.WriteLogToFile($"图片删除完成,已恢复到编辑模式: {previousEditingMode}"); } } @@ -2029,7 +2470,7 @@ namespace Ink_Canvas { try { - if (currentSelectedElement is Image image && sender is Ellipse ellipse) + if (IsBitmapLikeCanvasElement(currentSelectedElement) && sender is Ellipse ellipse) { isResizingImage = true; imageResizeStartPoint = e.GetPosition(inkCanvas); @@ -2072,10 +2513,10 @@ namespace Ink_Canvas { try { - if (isResizingImage && currentSelectedElement is Image image && sender is Ellipse ellipse) + if (isResizingImage && IsBitmapLikeCanvasElement(currentSelectedElement) && sender is Ellipse ellipse) { var currentPoint = e.GetPosition(inkCanvas); - ResizeImageByHandle(image, imageResizeStartPoint, currentPoint, activeResizeHandle); + ResizeImageByHandle(currentSelectedElement, imageResizeStartPoint, currentPoint, activeResizeHandle); imageResizeStartPoint = currentPoint; e.Handled = true; } @@ -2087,11 +2528,11 @@ namespace Ink_Canvas } // 根据控制点缩放图片 - private void ResizeImageByHandle(Image image, Point startPoint, Point currentPoint, string handleName) + private void ResizeImageByHandle(FrameworkElement element, Point startPoint, Point currentPoint, string handleName) { try { - if (image.RenderTransform is TransformGroup transformGroup) + if (element.RenderTransform is TransformGroup transformGroup) { var scaleTransform = transformGroup.Children.OfType().FirstOrDefault(); var translateTransform = transformGroup.Children.OfType().FirstOrDefault(); @@ -2099,7 +2540,7 @@ namespace Ink_Canvas if (scaleTransform == null || translateTransform == null) return; // 获取图片的当前边界 - Rect currentBounds = GetElementActualBounds(image); + Rect currentBounds = GetElementActualBounds(element); double deltaX = currentPoint.X - startPoint.X; double deltaY = currentPoint.Y - startPoint.Y; @@ -2160,7 +2601,10 @@ namespace Ink_Canvas translateTransform.Y += translateY; // 更新选择点位置 - UpdateImageResizeHandlesPosition(GetElementActualBounds(image)); + UpdateImageResizeHandlesPosition(GetElementActualBounds(element)); + + if (BorderImageSelectionControl?.Visibility == Visibility.Visible) + UpdateImageSelectionToolbarPosition(element); } } catch (Exception ex) diff --git a/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs b/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs index e3c1f7e2..2788a77f 100644 --- a/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs +++ b/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs @@ -1,3 +1,4 @@ +using Ink_Canvas.Controls; using Ink_Canvas.Helpers; using iNKORE.UI.WPF.Modern; using System; @@ -18,7 +19,6 @@ using Application = System.Windows.Application; using Button = System.Windows.Controls.Button; using Cursors = System.Windows.Input.Cursors; using HorizontalAlignment = System.Windows.HorizontalAlignment; -using Image = System.Windows.Controls.Image; using MessageBox = iNKORE.UI.WPF.Modern.Controls.MessageBox; using MouseEventArgs = System.Windows.Input.MouseEventArgs; using OpenFileDialog = Microsoft.Win32.OpenFileDialog; @@ -370,7 +370,7 @@ namespace Ink_Canvas /// /// 是否自動居中浮動工具欄 /// - private async void HideSubPanels(string mode = null, bool autoAlignCenter = false) + internal async void HideSubPanels(string mode = null, bool autoAlignCenter = false) { AnimationsHelper.HideWithSlideAndFade(BorderTools); AnimationsHelper.HideWithSlideAndFade(BoardBorderTools); @@ -3177,7 +3177,7 @@ namespace Ink_Canvas /// /// 发送者 /// 路由事件参数 - private async void BtnSettings_Click(object sender, RoutedEventArgs e) + internal async void BtnSettings_Click(object sender, RoutedEventArgs e) { if (BorderSettings.Visibility == Visibility.Visible) { @@ -3298,6 +3298,9 @@ namespace Ink_Canvas // 清空触摸点计数器 dec.Clear(); + if (isPalmEraserActive) + isPalmEraserActive = false; + // 确保触摸事件能正常响应 inkCanvas.IsHitTestVisible = true; inkCanvas.IsManipulationEnabled = true; @@ -3567,6 +3570,8 @@ namespace Ink_Canvas break; } } + + SyncPdfPageSidebarWithCanvas(); } private int BoundsWidth = 5; @@ -3774,32 +3779,29 @@ namespace Ink_Canvas // Open file dialog to select image var dialog = new OpenFileDialog { - Filter = "图片文件|*.jpg;*.jpeg;*.png;*.bmp;*.gif" + Filter = "图片与 PDF|*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.pdf|图片文件|*.jpg;*.jpeg;*.png;*.bmp;*.gif|PDF|*.pdf" }; if (dialog.ShowDialog() == true) { string filePath = dialog.FileName; - Image image = await CreateAndCompressImageAsync(filePath); - if (image != null) + FrameworkElement element = await CreateAndCompressImageAsync(filePath); + if (element != null) { string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff"); - image.Name = timestamp; + element.Name = timestamp; // 初始化TransformGroup - if (image is FrameworkElement element) - { - var transformGroup = new TransformGroup(); - transformGroup.Children.Add(new ScaleTransform(1, 1)); - transformGroup.Children.Add(new TranslateTransform(0, 0)); - transformGroup.Children.Add(new RotateTransform(0)); - element.RenderTransform = transformGroup; - } + var transformGroup = new TransformGroup(); + transformGroup.Children.Add(new ScaleTransform(1, 1)); + transformGroup.Children.Add(new TranslateTransform(0, 0)); + transformGroup.Children.Add(new RotateTransform(0)); + element.RenderTransform = transformGroup; - CenterAndScaleElement(image); + CenterAndScaleElement(element); // 设置图片属性,避免被InkCanvas选择系统处理 - image.IsHitTestVisible = true; - image.Focusable = false; + element.IsHitTestVisible = true; + element.Focusable = false; // 初始化InkCanvas选择设置 if (inkCanvas != null) @@ -3810,34 +3812,33 @@ namespace Ink_Canvas inkCanvas.EditingMode = InkCanvasEditingMode.None; } - inkCanvas.Children.Add(image); + inkCanvas.Children.Add(element); // 绑定事件处理器 - if (image is FrameworkElement elementForEvents) - { - // 鼠标事件 - elementForEvents.MouseLeftButtonDown += Element_MouseLeftButtonDown; - elementForEvents.MouseLeftButtonUp += Element_MouseLeftButtonUp; - elementForEvents.MouseMove += Element_MouseMove; - elementForEvents.MouseWheel += Element_MouseWheel; + element.MouseLeftButtonDown += Element_MouseLeftButtonDown; + element.MouseLeftButtonUp += Element_MouseLeftButtonUp; + element.MouseMove += Element_MouseMove; + element.MouseWheel += Element_MouseWheel; - // 触摸事件 - elementForEvents.TouchDown += Element_TouchDown; - elementForEvents.TouchUp += Element_TouchUp; - elementForEvents.IsManipulationEnabled = true; - elementForEvents.ManipulationDelta += Element_ManipulationDelta; - elementForEvents.ManipulationCompleted += Element_ManipulationCompleted; + // 触摸事件 + element.TouchDown += Element_TouchDown; + element.TouchUp += Element_TouchUp; + element.IsManipulationEnabled = true; + element.ManipulationDelta += Element_ManipulationDelta; + element.ManipulationCompleted += Element_ManipulationCompleted; - // 设置光标 - elementForEvents.Cursor = Cursors.Hand; - } + // 设置光标 + element.Cursor = Cursors.Hand; - timeMachine.CommitElementInsertHistory(image); + timeMachine.CommitElementInsertHistory(element); // 插入图片后切换到选择模式并刷新浮动栏高光显示 SetCurrentToolMode(InkCanvasEditingMode.Select); UpdateCurrentToolMode("select"); HideSubPanels("select"); + if (element is PdfEmbeddedView) + _pdfSidebarNextPositionUseHostTransform = true; + SyncPdfPageSidebarWithCanvas(); } } } @@ -3847,32 +3848,29 @@ namespace Ink_Canvas { var dialog = new OpenFileDialog { - Filter = "图片文件|*.jpg;*.jpeg;*.png;*.bmp;*.gif" + Filter = "图片与 PDF|*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.pdf|图片文件|*.jpg;*.jpeg;*.png;*.bmp;*.gif|PDF|*.pdf" }; if (dialog.ShowDialog() == true) { string filePath = dialog.FileName; - Image image = await CreateAndCompressImageAsync(filePath); - if (image != null) + FrameworkElement element = await CreateAndCompressImageAsync(filePath); + if (element != null) { string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff"); - image.Name = timestamp; + element.Name = timestamp; // 初始化TransformGroup - if (image is FrameworkElement element) - { - var transformGroup = new TransformGroup(); - transformGroup.Children.Add(new ScaleTransform(1, 1)); - transformGroup.Children.Add(new TranslateTransform(0, 0)); - transformGroup.Children.Add(new RotateTransform(0)); - element.RenderTransform = transformGroup; - } + var transformGroup = new TransformGroup(); + transformGroup.Children.Add(new ScaleTransform(1, 1)); + transformGroup.Children.Add(new TranslateTransform(0, 0)); + transformGroup.Children.Add(new RotateTransform(0)); + element.RenderTransform = transformGroup; - CenterAndScaleElement(image); + CenterAndScaleElement(element); // 设置图片属性,避免被InkCanvas选择系统处理 - image.IsHitTestVisible = true; - image.Focusable = false; + element.IsHitTestVisible = true; + element.Focusable = false; // 初始化InkCanvas选择设置 if (inkCanvas != null) @@ -3883,34 +3881,33 @@ namespace Ink_Canvas inkCanvas.EditingMode = InkCanvasEditingMode.None; } - inkCanvas.Children.Add(image); + inkCanvas.Children.Add(element); // 绑定事件处理器 - if (image is FrameworkElement elementForEvents) - { - // 鼠标事件 - elementForEvents.MouseLeftButtonDown += Element_MouseLeftButtonDown; - elementForEvents.MouseLeftButtonUp += Element_MouseLeftButtonUp; - elementForEvents.MouseMove += Element_MouseMove; - elementForEvents.MouseWheel += Element_MouseWheel; + element.MouseLeftButtonDown += Element_MouseLeftButtonDown; + element.MouseLeftButtonUp += Element_MouseLeftButtonUp; + element.MouseMove += Element_MouseMove; + element.MouseWheel += Element_MouseWheel; - // 触摸事件 - elementForEvents.TouchDown += Element_TouchDown; - elementForEvents.TouchUp += Element_TouchUp; - elementForEvents.IsManipulationEnabled = true; - elementForEvents.ManipulationDelta += Element_ManipulationDelta; - elementForEvents.ManipulationCompleted += Element_ManipulationCompleted; + // 触摸事件 + element.TouchDown += Element_TouchDown; + element.TouchUp += Element_TouchUp; + element.IsManipulationEnabled = true; + element.ManipulationDelta += Element_ManipulationDelta; + element.ManipulationCompleted += Element_ManipulationCompleted; - // 设置光标 - elementForEvents.Cursor = Cursors.Hand; - } + // 设置光标 + element.Cursor = Cursors.Hand; - timeMachine.CommitElementInsertHistory(image); + timeMachine.CommitElementInsertHistory(element); // 插入图片后切换到选择模式并刷新浮动栏高光显示 SetCurrentToolMode(InkCanvasEditingMode.Select); UpdateCurrentToolMode("select"); HideSubPanels("select"); + if (element is PdfEmbeddedView) + _pdfSidebarNextPositionUseHostTransform = true; + SyncPdfPageSidebarWithCanvas(); } } } @@ -3920,32 +3917,29 @@ namespace Ink_Canvas { var dialog = new OpenFileDialog { - Filter = "图片文件|*.jpg;*.jpeg;*.png;*.bmp;*.gif" + Filter = "图片与 PDF|*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.pdf|图片文件|*.jpg;*.jpeg;*.png;*.bmp;*.gif|PDF|*.pdf" }; if (dialog.ShowDialog() == true) { string filePath = dialog.FileName; - Image image = await CreateAndCompressImageAsync(filePath); // 补充image定义 - if (image != null) + FrameworkElement element = await CreateAndCompressImageAsync(filePath); + if (element != null) { string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff"); - image.Name = timestamp; + element.Name = timestamp; // 初始化TransformGroup - if (image is FrameworkElement element) - { - var transformGroup = new TransformGroup(); - transformGroup.Children.Add(new ScaleTransform(1, 1)); - transformGroup.Children.Add(new TranslateTransform(0, 0)); - transformGroup.Children.Add(new RotateTransform(0)); - element.RenderTransform = transformGroup; - } + var transformGroup = new TransformGroup(); + transformGroup.Children.Add(new ScaleTransform(1, 1)); + transformGroup.Children.Add(new TranslateTransform(0, 0)); + transformGroup.Children.Add(new RotateTransform(0)); + element.RenderTransform = transformGroup; - CenterAndScaleElement(image); + CenterAndScaleElement(element); // 设置图片属性,避免被InkCanvas选择系统处理 - image.IsHitTestVisible = true; - image.Focusable = false; + element.IsHitTestVisible = true; + element.Focusable = false; // 初始化InkCanvas选择设置 if (inkCanvas != null) @@ -3956,34 +3950,33 @@ namespace Ink_Canvas inkCanvas.EditingMode = InkCanvasEditingMode.None; } - inkCanvas.Children.Add(image); + inkCanvas.Children.Add(element); // 绑定事件处理器 - if (image is FrameworkElement elementForEvents) - { - // 鼠标事件 - elementForEvents.MouseLeftButtonDown += Element_MouseLeftButtonDown; - elementForEvents.MouseLeftButtonUp += Element_MouseLeftButtonUp; - elementForEvents.MouseMove += Element_MouseMove; - elementForEvents.MouseWheel += Element_MouseWheel; + element.MouseLeftButtonDown += Element_MouseLeftButtonDown; + element.MouseLeftButtonUp += Element_MouseLeftButtonUp; + element.MouseMove += Element_MouseMove; + element.MouseWheel += Element_MouseWheel; - // 触摸事件 - elementForEvents.TouchDown += Element_TouchDown; - elementForEvents.TouchUp += Element_TouchUp; - elementForEvents.IsManipulationEnabled = true; - elementForEvents.ManipulationDelta += Element_ManipulationDelta; - elementForEvents.ManipulationCompleted += Element_ManipulationCompleted; + // 触摸事件 + element.TouchDown += Element_TouchDown; + element.TouchUp += Element_TouchUp; + element.IsManipulationEnabled = true; + element.ManipulationDelta += Element_ManipulationDelta; + element.ManipulationCompleted += Element_ManipulationCompleted; - // 设置光标 - elementForEvents.Cursor = Cursors.Hand; - } + // 设置光标 + element.Cursor = Cursors.Hand; - timeMachine.CommitElementInsertHistory(image); + timeMachine.CommitElementInsertHistory(element); // 插入图片后切换到选择模式并刷新浮动栏高光显示 SetCurrentToolMode(InkCanvasEditingMode.Select); UpdateCurrentToolMode("select"); HideSubPanels("select"); + if (element is PdfEmbeddedView) + _pdfSidebarNextPositionUseHostTransform = true; + SyncPdfPageSidebarWithCanvas(); } } } diff --git a/Ink Canvas/MainWindow_cs/MW_PPT.cs b/Ink Canvas/MainWindow_cs/MW_PPT.cs index 00101bd5..e9fd850f 100644 --- a/Ink Canvas/MainWindow_cs/MW_PPT.cs +++ b/Ink Canvas/MainWindow_cs/MW_PPT.cs @@ -4,6 +4,7 @@ using Microsoft.Office.Core; using Microsoft.Office.Interop.PowerPoint; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Text; @@ -167,6 +168,18 @@ namespace Ink_Canvas /// 断开连接后退出PPT模式的延迟时间(毫秒),即连接断开后多长时间才退出PPT模式。 /// private const int ExitPPTModeAfterDisconnectDelayMs = 1200; + + /// + /// 仅PPT模式下周期性探测放映界面(COM 失效时依赖 Win32),间隔不宜过小以免多余开销。 + /// + private DispatcherTimer _pptOnlyVisibilityProbeTimer; + + private const int PptOnlyVisibilityProbeIntervalMs = 800; + + /// + /// PowerPoint 全屏放映顶层窗口类名(与编辑态 PPTFrameClass 区分)。 + /// + private const string PowerPointSlideShowWindowClassName = "screenClass"; #endregion #region PPT Managers @@ -638,6 +651,8 @@ namespace Ink_Canvas ClosePowerPointApplication(); ClearStaticInteropState(); + StopPptOnlyVisibilityProbeTimer(); + LogHelper.WriteLogToFile("PPT管理器已释放", LogHelper.LogType.Event); } catch (Exception ex) @@ -700,6 +715,104 @@ namespace Ink_Canvas } #endregion + #region 仅PPT模式可见性(COM + Win32 兜底) + + /// + /// 在启用「仅PPT模式」时启动轻量探测,COM 事件延迟或失效时仍可根据全屏放映窗口显示主窗口。 + /// + internal void EnsurePptOnlyVisibilityProbeTimer() + { + try + { + if (!Settings.ModeSettings.IsPPTOnlyMode) + { + StopPptOnlyVisibilityProbeTimer(); + return; + } + + if (_pptOnlyVisibilityProbeTimer == null) + { + _pptOnlyVisibilityProbeTimer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(PptOnlyVisibilityProbeIntervalMs) + }; + _pptOnlyVisibilityProbeTimer.Tick += (_, __) => CheckMainWindowVisibility(); + } + + if (!_pptOnlyVisibilityProbeTimer.IsEnabled) + _pptOnlyVisibilityProbeTimer.Start(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"仅PPT可见性探测计时器启动失败: {ex.Message}", LogHelper.LogType.Warning); + } + } + + internal void StopPptOnlyVisibilityProbeTimer() + { + try + { + _pptOnlyVisibilityProbeTimer?.Stop(); + } + catch + { + } + } + + /// + /// 检测是否存在 PowerPoint 全屏放映顶层窗口(类名 screenClass,进程 powerpnt),用于 COM 不可用时的兜底。 + /// + internal bool IsPowerPointSlideshowSurfacePresentWin32() + { + if (!Settings.ModeSettings.IsPPTOnlyMode) + return false; + + try + { + bool found = false; + EnumWindows((hWnd, _) => + { + if (!IsWindow(hWnd) || !IsWindowVisible(hWnd)) + return true; + + var cls = new StringBuilder(256); + if (GetClassName(hWnd, cls, cls.Capacity) == 0) + return true; + + if (!string.Equals(cls.ToString(), PowerPointSlideShowWindowClassName, StringComparison.OrdinalIgnoreCase)) + return true; + + try + { + GetWindowThreadProcessId(hWnd, out uint pid); + using (var proc = Process.GetProcessById((int)pid)) + { + var name = proc.ProcessName; + if (string.Equals(name, "POWERPNT", StringComparison.OrdinalIgnoreCase)) + { + found = true; + return false; + } + } + } + catch + { + } + + return true; + }, IntPtr.Zero); + + return found; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"Win32 检测 PPT 放映窗口失败: {ex.Message}", LogHelper.LogType.Trace); + return false; + } + } + + #endregion + #region New PPT Event Handlers /// /// 处理 PowerPoint 连接状态的变更:更新界面连接/放映状态,并在断开时启动一个短延迟以安全退出 PPT 模式。 @@ -731,6 +844,8 @@ namespace Ink_Canvas _ = HandleManualSlideShowEnd(); if (Settings.PowerPointSettings.UseRotPptLink) _pptManager?.ReloadConnection(); + + CheckMainWindowVisibility(); } }); } @@ -1114,6 +1229,9 @@ namespace Ink_Canvas // 加载当前页墨迹 LoadCurrentSlideInk(currentSlide); + + // 仅PPT模式:放映开始立即同步主窗口可见性(勿仅依赖 SlideShowStateChanged 定时器) + CheckMainWindowVisibility(); }); if (!isFloatingBarFolded) @@ -1399,6 +1517,8 @@ namespace Ink_Canvas currentMode = 0; } + SyncPdfPageSidebarWithCanvas(); + ClearStrokes(true); // 清空备份历史记录,防止退出白板时恢复已结束PPT的墨迹 // 注意:这里只清空索引0的备份,不影响白板页面的墨迹(索引1及以上) @@ -1435,6 +1555,8 @@ namespace Ink_Canvas UpdateCurrentToolMode("cursor"); SetFloatingBarHighlightPosition("cursor"); + + CheckMainWindowVisibility(); } catch (Exception ex) { @@ -2291,6 +2413,7 @@ namespace Ink_Canvas _pptUIManager?.UpdateSlideShowStatus(false); _pptUIManager?.UpdateSidebarExitButtons(false); LogHelper.WriteLogToFile("手动更新放映结束UI状态", LogHelper.LogType.Trace); + CheckMainWindowVisibility(); }); // 手动处理自动收纳,因为OnPPTSlideShowEnd事件可能未触发 @@ -2323,6 +2446,7 @@ namespace Ink_Canvas { _pptUIManager?.UpdateSlideShowStatus(false); _pptUIManager?.UpdateSidebarExitButtons(false); + CheckMainWindowVisibility(); }); // 异常情况下也手动处理自动收纳 diff --git a/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs b/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs index e55d8e15..bb42d7c5 100644 --- a/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs +++ b/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs @@ -1,3 +1,4 @@ +using Ink_Canvas.Controls; using Ink_Canvas.Helpers; using Newtonsoft.Json; using System; @@ -16,6 +17,7 @@ using System.Windows.Ink; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; +using System.Windows.Threading; using System.Xml; using System.Xml.Linq; using Color = System.Drawing.Color; @@ -28,16 +30,58 @@ namespace Ink_Canvas // 1. 定义元素信息结构 public class CanvasElementInfo { - public string Type { get; set; } // "Image" + public string Type { get; set; } // "Image" | "Pdf" public string SourcePath { get; set; } public double Left { get; set; } public double Top { get; set; } public double Width { get; set; } public double Height { get; set; } public string Stretch { get; set; } = "Fill"; // 默认为Fill + /// PDF 当前页(从 0 开始),仅 Type == Pdf 时有效。 + public int? PdfCurrentPage { get; set; } + /// 保存时的 PDF 总页数,用于校验;仅 Type == Pdf 时有效。 + public int? PdfPageCount { get; set; } } public partial class MainWindow : Window { + /// 收集画布上图片与 PDF 的元数据,写入 .elements.json(与墨迹文件同路径)。 + private void CollectCanvasElementsMetadata(List elementInfos) + { + if (elementInfos == null || inkCanvas == null) return; + + foreach (var child in inkCanvas.Children) + { + if (child is Image img && img.Source is BitmapImage bmp) + { + elementInfos.Add(new CanvasElementInfo + { + Type = "Image", + SourcePath = bmp.UriSource?.LocalPath ?? "", + Left = InkCanvas.GetLeft(img), + Top = InkCanvas.GetTop(img), + Width = img.Width, + Height = img.Height, + Stretch = img.Stretch.ToString() + }); + } + else if (child is PdfEmbeddedView pdf && !string.IsNullOrEmpty(pdf.PdfPath)) + { + elementInfos.Add(new CanvasElementInfo + { + Type = "Pdf", + SourcePath = pdf.PdfPath, + Left = InkCanvas.GetLeft(pdf), + Top = InkCanvas.GetTop(pdf), + Width = pdf.Width, + Height = pdf.Height, + Stretch = "Uniform", + PdfCurrentPage = (int)pdf.CurrentPageIndex, + PdfPageCount = (int)pdf.PageCount + }); + } + } + } + /// /// 保存墨迹的鼠标释放事件处理 /// @@ -419,22 +463,7 @@ namespace Ink_Canvas // 保存元素信息 var elementInfos = new List(); - foreach (var child in inkCanvas.Children) - { - if (child is Image img && img.Source is BitmapImage bmp) - { - elementInfos.Add(new CanvasElementInfo - { - Type = "Image", - SourcePath = bmp.UriSource?.LocalPath ?? "", - Left = InkCanvas.GetLeft(img), - Top = InkCanvas.GetTop(img), - Width = img.Width, - Height = img.Height, - Stretch = img.Stretch.ToString() - }); - } - } + CollectCanvasElementsMetadata(elementInfos); string elementsPath = Settings.Automation.IsSaveStrokesAsXML ? Path.ChangeExtension(savePathWithName, ".elements.json") : Path.ChangeExtension(savePathWithName, ".elements.json"); File.WriteAllText(elementsPath, JsonConvert.SerializeObject(elementInfos, Newtonsoft.Json.Formatting.Indented)); } @@ -485,22 +514,7 @@ namespace Ink_Canvas // 同时保存元素信息 var elementInfos = new List(); - foreach (var child in inkCanvas.Children) - { - if (child is Image img && img.Source is BitmapImage bmp) - { - elementInfos.Add(new CanvasElementInfo - { - Type = "Image", - SourcePath = bmp.UriSource?.LocalPath ?? "", - Left = InkCanvas.GetLeft(img), - Top = InkCanvas.GetTop(img), - Width = img.Width, - Height = img.Height, - Stretch = img.Stretch.ToString() - }); - } - } + CollectCanvasElementsMetadata(elementInfos); File.WriteAllText(Path.ChangeExtension(xmlPath, ".elements.json"), JsonConvert.SerializeObject(elementInfos, Newtonsoft.Json.Formatting.Indented)); // 异步上传到Dlass @@ -1266,6 +1280,10 @@ namespace Ink_Canvas InkCanvas.SetTop(img, info.Top); inkCanvas.Children.Add(img); } + else if (string.Equals(info.Type, "Pdf", StringComparison.OrdinalIgnoreCase) && File.Exists(info.SourcePath)) + { + Dispatcher.BeginInvoke(new Action(() => { _ = RestorePdfFromElementInfoAsync(info); }), DispatcherPriority.Loaded); + } } } } @@ -1416,6 +1434,10 @@ namespace Ink_Canvas InkCanvas.SetTop(img, info.Top); inkCanvas.Children.Add(img); } + else if (string.Equals(info.Type, "Pdf", StringComparison.OrdinalIgnoreCase) && File.Exists(info.SourcePath)) + { + Dispatcher.BeginInvoke(new Action(() => { _ = RestorePdfFromElementInfoAsync(info); }), DispatcherPriority.Loaded); + } } } diff --git a/Ink Canvas/MainWindow_cs/MW_SelectionGestures.cs b/Ink Canvas/MainWindow_cs/MW_SelectionGestures.cs index 6ee5dc49..92ee533c 100644 --- a/Ink Canvas/MainWindow_cs/MW_SelectionGestures.cs +++ b/Ink Canvas/MainWindow_cs/MW_SelectionGestures.cs @@ -1,3 +1,4 @@ +using Ink_Canvas.Controls; using Ink_Canvas.Helpers; using iNKORE.UI.WPF.Controls; using System; @@ -610,7 +611,7 @@ namespace Ink_Canvas // 检查是否有图片元素被选中(通过InkCanvas的选中元素) var selectedElements = inkCanvas.GetSelectedElements(); - bool hasImageElement = selectedElements.Any(element => element is Image); + bool hasImageElement = selectedElements.Any(element => element is Image || element is PdfEmbeddedView); // 如果有图片元素被选中,不显示选择框 if (hasImageElement) @@ -621,7 +622,7 @@ namespace Ink_Canvas } // 检查是否有图片元素被选中(通过currentSelectedElement) - if (currentSelectedElement != null && currentSelectedElement is Image) + if (currentSelectedElement != null && (currentSelectedElement is Image || currentSelectedElement is PdfEmbeddedView)) { GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed; HideSelectionDisplay(); diff --git a/Ink Canvas/MainWindow_cs/MW_Settings.cs b/Ink Canvas/MainWindow_cs/MW_Settings.cs index cedea1b4..acacfedc 100644 --- a/Ink Canvas/MainWindow_cs/MW_Settings.cs +++ b/Ink Canvas/MainWindow_cs/MW_Settings.cs @@ -1,4 +1,4 @@ -using Hardcodet.Wpf.TaskbarNotification; +using H.NotifyIcon; using Ink_Canvas.Helpers; using Newtonsoft.Json; using OSVersionExtension; @@ -3597,6 +3597,11 @@ namespace Ink_Canvas RestoreNonStrokeElements(preservedElements); isInMultiTouchMode = true; + palmEraserWasEnabledBeforeMultiTouch = Settings.Canvas.EnablePalmEraser; + Settings.Canvas.EnablePalmEraser = false; + if (ToggleSwitchEnablePalmEraser != null) + ToggleSwitchEnablePalmEraser.IsOn = false; + // 恢复到之前的编辑状态 inkCanvas.EditingMode = currentEditingMode; drawingShapeMode = currentDrawingShapeMode; @@ -3627,6 +3632,13 @@ namespace Ink_Canvas RestoreNonStrokeElements(preservedElements); isInMultiTouchMode = false; + if (palmEraserWasEnabledBeforeMultiTouch) + { + Settings.Canvas.EnablePalmEraser = true; + if (ToggleSwitchEnablePalmEraser != null) + ToggleSwitchEnablePalmEraser.IsOn = true; + } + // 恢复到之前的编辑状态 inkCanvas.EditingMode = currentEditingMode; drawingShapeMode = currentDrawingShapeMode; diff --git a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs index 13277e7a..9fbca2bc 100644 --- a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs +++ b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs @@ -1,4 +1,4 @@ -using Hardcodet.Wpf.TaskbarNotification; +using H.NotifyIcon; using Ink_Canvas.Helpers; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -1502,7 +1502,7 @@ namespace Ink_Canvas Settings defaultSettings = new Settings(); // 将默认配置和用户配置都序列化为JObject - JObject defaultConfigObj = JObject.FromObject(defaultSettings); EnsureDefaultConfigSchemaIncludesIgnoredNullKeys(defaultConfigObj); + JObject defaultConfigObj = JObject.FromObject(defaultSettings); EnsureDefaultConfigSchemaIncludesIgnoredNullKeys(defaultConfigObj); JObject userConfigObj = JObject.Parse(userConfigJson); // 记录是否有清理操作 diff --git a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs index fc304e72..eb85e37f 100644 --- a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs +++ b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs @@ -142,14 +142,24 @@ namespace Ink_Canvas } } - private void RunStrokeCollectedPostShapeRecognitionTail(InkCanvasStrokeCollectedEventArgs e, bool wasStraightened) + /// + /// 收笔后压感/墨迹平滑等尾部处理。返回「当前应登记到手写字形替换批次」的画布笔画引用: + /// 同步贝塞尔平滑若替换了笔画,则为新 ;否则为 .Stroke。 + /// 直线拉直后事件参数中的笔画可能已不在画布上,调用方需另行传入画布上的笔画(见收笔处)。 + /// + private Stroke RunStrokeCollectedPostShapeRecognitionTail(InkCanvasStrokeCollectedEventArgs e, bool wasStraightened) { + if (e?.Stroke == null) + return null; + + var handwritingScheduleStroke = e.Stroke; + try { foreach (var stylusPoint in e.Stroke.StylusPoints) if ((stylusPoint.PressureFactor > 0.501 || stylusPoint.PressureFactor < 0.5) && stylusPoint.PressureFactor != 0) - return; + return e.Stroke; try { @@ -166,85 +176,89 @@ namespace Ink_Canvas } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } - switch (Settings.Canvas.InkStyle) + // 「屏蔽压感」已在收笔主路径将点集归一成 0.5;此处若再跑 InkStyle 0/1 会重写 PressureFactor,造成假压感。 + if (!Settings.Canvas.DisablePressure) { - case 1: - if (penType == 0) - try - { - var stylusPoints = new StylusPointCollection(); - var n = e.Stroke.StylusPoints.Count - 1; - - for (var i = 0; i <= n; i++) + switch (Settings.Canvas.InkStyle) + { + case 1: + if (penType == 0) + try { - var speed = GetPointSpeed(e.Stroke.StylusPoints[Math.Max(i - 1, 0)].ToPoint(), - e.Stroke.StylusPoints[i].ToPoint(), - e.Stroke.StylusPoints[Math.Min(i + 1, n)].ToPoint()); - var point = new StylusPoint - { - PressureFactor = RateBasedPressureFactorFromPointSpeed(speed), - X = e.Stroke.StylusPoints[i].X, - Y = e.Stroke.StylusPoints[i].Y - }; - stylusPoints.Add(point); - } + var stylusPoints = new StylusPointCollection(); + var n = e.Stroke.StylusPoints.Count - 1; - e.Stroke.StylusPoints = stylusPoints; - } - catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } - - break; - case 0: - if (penType == 0) - try - { - var stylusPoints = new StylusPointCollection(); - var n = e.Stroke.StylusPoints.Count - 1; - var pressure = 0.1; - var x = 10; - if (n == 1) return; - if (n >= x) - { - for (var i = 0; i < n - x; i++) - { - var point = new StylusPoint(); - - point.PressureFactor = (float)0.5; - point.X = e.Stroke.StylusPoints[i].X; - point.Y = e.Stroke.StylusPoints[i].Y; - stylusPoints.Add(point); - } - - for (var i = n - x; i <= n; i++) - { - var point = new StylusPoint(); - - point.PressureFactor = (float)((0.5 - pressure) * (n - i) / x + pressure); - point.X = e.Stroke.StylusPoints[i].X; - point.Y = e.Stroke.StylusPoints[i].Y; - stylusPoints.Add(point); - } - } - else - { for (var i = 0; i <= n; i++) { - var point = new StylusPoint(); - - point.PressureFactor = (float)(0.4 * (n - i) / n + pressure); - point.X = e.Stroke.StylusPoints[i].X; - point.Y = e.Stroke.StylusPoints[i].Y; + var speed = GetPointSpeed(e.Stroke.StylusPoints[Math.Max(i - 1, 0)].ToPoint(), + e.Stroke.StylusPoints[i].ToPoint(), + e.Stroke.StylusPoints[Math.Min(i + 1, n)].ToPoint()); + var point = new StylusPoint + { + PressureFactor = RateBasedPressureFactorFromPointSpeed(speed), + X = e.Stroke.StylusPoints[i].X, + Y = e.Stroke.StylusPoints[i].Y + }; stylusPoints.Add(point); } + + e.Stroke.StylusPoints = stylusPoints; } + catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } - e.Stroke.StylusPoints = stylusPoints; - } - catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } + break; + case 0: + if (penType == 0) + try + { + var stylusPoints = new StylusPointCollection(); + var n = e.Stroke.StylusPoints.Count - 1; + var pressure = 0.1; + var x = 10; + if (n == 1) return e.Stroke; + if (n >= x) + { + for (var i = 0; i < n - x; i++) + { + var point = new StylusPoint(); - break; - case 3: - break; + point.PressureFactor = (float)0.5; + point.X = e.Stroke.StylusPoints[i].X; + point.Y = e.Stroke.StylusPoints[i].Y; + stylusPoints.Add(point); + } + + for (var i = n - x; i <= n; i++) + { + var point = new StylusPoint(); + + point.PressureFactor = (float)((0.5 - pressure) * (n - i) / x + pressure); + point.X = e.Stroke.StylusPoints[i].X; + point.Y = e.Stroke.StylusPoints[i].Y; + stylusPoints.Add(point); + } + } + else + { + for (var i = 0; i <= n; i++) + { + var point = new StylusPoint(); + + point.PressureFactor = (float)(0.4 * (n - i) / n + pressure); + point.X = e.Stroke.StylusPoints[i].X; + point.Y = e.Stroke.StylusPoints[i].Y; + stylusPoints.Add(point); + } + } + + e.Stroke.StylusPoints = stylusPoints; + } + catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } + + break; + case 3: + break; + } } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } @@ -276,6 +290,7 @@ namespace Ink_Canvas inkCanvas.Strokes.Remove(e.Stroke); inkCanvas.Strokes.Add(smoothedStroke); _currentCommitType = CommitReason.UserInput; + handwritingScheduleStroke = smoothedStroke; } } } @@ -293,6 +308,8 @@ namespace Ink_Canvas { drawingAttributes.FitToCurve = true; } + + return handwritingScheduleStroke; } /// @@ -325,6 +342,10 @@ namespace Ink_Canvas && Math.Abs(strokeDrawingAttributes.Width - BoardBrushInkWidth) < 0.01 && Math.Abs(strokeDrawingAttributes.Height - BoardBrushInkHeight) < 0.01; + // 手写识别须与画布显示分离:在压感/触摸模拟/笔锋/直线拉直等修改 e.Stroke 之前快照原始落笔点集。 + var handwritingRawPointsForRecognizer = + CloneStylusPointCollectionForHandwritingInput(e.Stroke?.StylusPoints); + // 检查是否启用墨迹渐隐功能 if (Settings.Canvas.EnableInkFade && !isBoardBrushStroke) { @@ -372,6 +393,8 @@ namespace Ink_Canvas // 标记是否进行了直线拉直 bool wasStraightened = false; + StylusPointCollection preBrushHandwritingPoints = null; + Stroke strokeForHandwritingBeautify = null; if (Settings.Canvas.FitToCurve) drawingAttributes.FitToCurve = false; @@ -379,7 +402,7 @@ namespace Ink_Canvas { inkCanvas.Opacity = 1; var touchPressureSimulationApplied = false; - var preBrushHandwritingPoints = CloneStylusPointCollectionForHandwritingInput(e.Stroke?.StylusPoints); + preBrushHandwritingPoints = handwritingRawPointsForRecognizer; if (Settings.Canvas.DisablePressure) { @@ -488,7 +511,11 @@ namespace Ink_Canvas // 实时笔锋:勿依赖 DrawingAttributes.IgnorePressure。无压感触摸/鼠标等设备上,运行时仍可能为 true, // 会导致不进入逻辑或进入后渲染仍忽略 PressureFactor;具体在 ApplyVelocityBrushTipFromSpeed 内关闭。 + // 「屏蔽压感」时必须跳过:否则会重写 PressureFactor 并强制 IgnorePressure=false,与归一压感冲突。 + // VelocityBrushTipMix <= 0 时 ApplyVelocityBrushTipFromSpeed 为空操作,无需调用。 if (Settings.Canvas.InkStyle == 3 + && Settings.Canvas.VelocityBrushTipMix > 0 + && !Settings.Canvas.DisablePressure && !touchPressureSimulationApplied && penType != 1 && e.Stroke?.DrawingAttributes != null @@ -500,6 +527,8 @@ namespace Ink_Canvas // Apply line straightening and endpoint snapping if ink-to-shape is enabled + Stroke straightStrokeForHandwritingKey = null; + if (Settings.InkToShape.IsInkToShapeEnabled) { // 检查是否启用了直线自动拉直功能 @@ -542,6 +571,8 @@ namespace Ink_Canvas inkCanvas.Strokes.Add(straightStroke); _currentCommitType = CommitReason.UserInput; + straightStrokeForHandwritingKey = straightStroke; + // We can't modify e.Stroke directly, but we need to update newStrokes // to ensure proper shape recognition for the straightened line if (newStrokes.Contains(e.Stroke)) @@ -555,12 +586,12 @@ namespace Ink_Canvas } } - Stroke strokeForHandwritingBeautify = e.Stroke; - if (wasStraightened && inkCanvas.Strokes.Count > 0) + strokeForHandwritingBeautify = e.Stroke; + if (wasStraightened && straightStrokeForHandwritingKey != null) + strokeForHandwritingBeautify = straightStrokeForHandwritingKey; + else if (wasStraightened && inkCanvas.Strokes.Count > 0) strokeForHandwritingBeautify = inkCanvas.Strokes[inkCanvas.Strokes.Count - 1]; - if (wasStraightened && strokeForHandwritingBeautify != null) - preBrushHandwritingPoints = CloneStylusPointCollectionForHandwritingInput(strokeForHandwritingBeautify.StylusPoints); if (ShapeRecognitionRouter.ShouldRunShapeRecognition( Settings.InkToShape.IsInkToShapeEnabled, @@ -912,9 +943,15 @@ namespace Ink_Canvas try { await InkToShapeProcessCoreAsync(); + var strokeAfterTail = RunStrokeCollectedPostShapeRecognitionTail(e, wsTail); if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) - ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeHw, isBoardBrushStroke, preBrushHwPts); - RunStrokeCollectedPostShapeRecognitionTail(e, wsTail); + { + var canvasStrokeForHw = wsTail ? strokeHw : strokeAfterTail; + ScheduleHandwritingGlyphReplaceAfterStrokeCollected( + canvasStrokeForHw, + isBoardBrushStroke, + preBrushHwPts); + } } catch (Exception ex) { @@ -927,14 +964,21 @@ namespace Ink_Canvas if (InkToShapeProcess()) return; } - else if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) - { - ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke, preBrushHandwritingPoints); - } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } - RunStrokeCollectedPostShapeRecognitionTail(e, wasStraightened); + var strokeAfterTailSync = RunStrokeCollectedPostShapeRecognitionTail(e, wasStraightened); + if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify + && !ShapeRecognitionRouter.ShouldRunShapeRecognition( + Settings.InkToShape.IsInkToShapeEnabled, + ShapeRecognitionRouter.FromSettingsInt(Settings.InkToShape.ShapeRecognitionEngine))) + { + var canvasStrokeForHw = wasStraightened ? strokeForHandwritingBeautify : strokeAfterTailSync; + ScheduleHandwritingGlyphReplaceAfterStrokeCollected( + canvasStrokeForHw, + isBoardBrushStroke, + preBrushHandwritingPoints); + } } /// @@ -969,6 +1013,8 @@ namespace Ink_Canvas inkCanvas.Strokes.Remove(original); inkCanvas.Strokes.Add(smoothed); _currentCommitType = CommitReason.UserInput; + // 收笔尾部仍以 original 登记手写批次;异步平滑后画布对象变为 smoothed,须迁移引用,否则防抖识别时字典 miss 会退回画布几何(非实时笔锋常见)。 + MigrateHandwritingBeautifyCanvasStrokeReference(original, smoothed); } else { @@ -2145,12 +2191,16 @@ namespace Ink_Canvas /// /// 将沿线速度映射为压感并与硬件压感混合,快写略细、慢写略粗;在落笔时(及手写笔移动时由调用方)统一施加。 - /// 无压感设备上系统可能将 置为 true,此处强制关闭以便粗细随合成压感变化(与「屏蔽压感」无关:调用方已保证未屏蔽)。 + /// 无压感设备上系统可能将 置为 true,此处强制关闭以便粗细随合成压感变化。 + /// 若 为 true,本方法直接返回且不修改 IgnorePressure。 /// private void ApplyVelocityBrushTipFromSpeed(Stroke stroke) { try { + if (Settings.Canvas.DisablePressure) + return; + var mix = Settings.Canvas.VelocityBrushTipMix; if (mix <= 0 || stroke == null) return; if (mix > 1) mix = 1; @@ -2732,6 +2782,29 @@ namespace Ink_Canvas return corners.OrderBy(p => Math.Atan2(p.Y - center.Y, p.X - center.X)).ToList(); } + /// + /// 异步墨迹平滑将画布上的 替换为 后,把手写字形替换批次里的画布引用一并迁移,使识别仍命中「原始点快照」字典项。 + /// + private void MigrateHandwritingBeautifyCanvasStrokeReference(Stroke fromStroke, Stroke toStroke) + { + if (fromStroke == null || toStroke == null || ReferenceEquals(fromStroke, toStroke)) + return; + if (!Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) + return; + + if (_handwritingBeautifyInkInputByCanvasStroke.TryGetValue(fromStroke, out var inkInput)) + { + _handwritingBeautifyInkInputByCanvasStroke.Remove(fromStroke); + _handwritingBeautifyInkInputByCanvasStroke[toStroke] = inkInput; + } + + for (var i = 0; i < _handwritingRecentStrokesForBeautify.Count; i++) + { + if (ReferenceEquals(_handwritingRecentStrokesForBeautify[i], fromStroke)) + _handwritingRecentStrokesForBeautify[i] = toStroke; + } + } + /// /// 收笔后:在墨迹转形状(若启用)完成之后,将笔画并入批次并启动/重置停笔防抖计时器,再于延迟后多笔合并矫正。 /// @@ -2790,10 +2863,19 @@ namespace Ink_Canvas if (preBrushHandwritingPoints != null && preBrushHandwritingPoints.Count > 0) { - _handwritingBeautifyInkInputByCanvasStroke[strokeForBeautify] = new Stroke(preBrushHandwritingPoints) + // 再拷贝一份给识别专用 Stroke,避免与外部 StylusPointCollection 或 WPF Stroke 内部共享后被改写。 + var ptsForRecognizer = CloneStylusPointCollectionForHandwritingInput(preBrushHandwritingPoints); + if (ptsForRecognizer != null && ptsForRecognizer.Count > 0) { - DrawingAttributes = strokeForBeautify.DrawingAttributes.Clone() - }; + _handwritingBeautifyInkInputByCanvasStroke[strokeForBeautify] = new Stroke(ptsForRecognizer) + { + DrawingAttributes = strokeForBeautify.DrawingAttributes.Clone() + }; + } + else + { + _handwritingBeautifyInkInputByCanvasStroke.Remove(strokeForBeautify); + } } else { @@ -2815,7 +2897,11 @@ namespace Ink_Canvas PruneHandwritingBeautifyBatch(); while (_handwritingRecentStrokesForBeautify.Count > HandwritingBeautifyMaxRecentStrokes) + { + var evicted = _handwritingRecentStrokesForBeautify[0]; _handwritingRecentStrokesForBeautify.RemoveAt(0); + _handwritingBeautifyInkInputByCanvasStroke.Remove(evicted); + } EnsureHandwritingBeautifyDebounceTimer(); _handwritingBeautifyDebounceTimer.Stop(); @@ -2858,9 +2944,17 @@ namespace Ink_Canvas continue; canvasStrokes.Add(s); if (_handwritingBeautifyInkInputByCanvasStroke.TryGetValue(s, out var inkInput) && inkInput != null) + { recognitionInput.Add(inkInput); + } else + { + LogHelper.WriteLogToFile( + "[手写体] 批次识别输入回退为画布笔画(未命中原始点快照)。画布点数=" + + (s.StylusPoints?.Count ?? 0), + LogHelper.LogType.Info); recognitionInput.Add(s); + } } if (canvasStrokes.Count == 0) diff --git a/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs b/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs index 7f44331e..dc70ea42 100644 --- a/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs +++ b/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs @@ -44,6 +44,9 @@ namespace Ink_Canvas /// 多点触控延迟时间(毫秒) /// private const double MULTI_TOUCH_DELAY_MS = 100; + private bool isMultiTouchTimerActive; + private bool isPalmEraserActive; + private bool palmEraserWasEnabledBeforeMultiTouch; /// /// 保存画布上的非笔画元素(如图片、媒体元素等) @@ -227,6 +230,12 @@ namespace Ink_Canvas RestoreNonStrokeElements(preservedElements); isInMultiTouchMode = false; + if (palmEraserWasEnabledBeforeMultiTouch) + { + Settings.Canvas.EnablePalmEraser = true; + if (ToggleSwitchEnablePalmEraser != null) + ToggleSwitchEnablePalmEraser.IsOn = true; + } } else { @@ -247,6 +256,11 @@ namespace Ink_Canvas // 恢复非笔画元素 RestoreNonStrokeElements(preservedElements); isInMultiTouchMode = true; + + palmEraserWasEnabledBeforeMultiTouch = Settings.Canvas.EnablePalmEraser; + Settings.Canvas.EnablePalmEraser = false; + if (ToggleSwitchEnablePalmEraser != null) + ToggleSwitchEnablePalmEraser.IsOn = false; } } @@ -543,10 +557,12 @@ namespace Ink_Canvas foreach (var stylusPoint in stylusPointCollection) strokeVisual.Add(new StylusPoint(stylusPoint.X, stylusPoint.Y, stylusPoint.PressureFactor)); - // 实时笔锋:在绘制过程中更新压感并整笔重绘预览;否则预览层固定线宽,收笔后改点集也看不到笔锋变化。 + // 实时笔锋:混合度 > 0 时在绘制过程中更新压感并整笔重绘预览;混合为 0 时与普通过程一致用增量 Redraw,避免每点 ForceRedraw 整笔清空(长笔画卡顿)。 var committedStroke = strokeVisual.Stroke; if (committedStroke != null && Settings.Canvas.InkStyle == 3 + && Settings.Canvas.VelocityBrushTipMix > 0 + && !Settings.Canvas.DisablePressure && penType == 0 && committedStroke.DrawingAttributes != null && !committedStroke.DrawingAttributes.IsHighlighter @@ -719,21 +735,13 @@ namespace Ink_Canvas /// 触摸事件参数 /// 返回触摸边界宽度 /// - /// 根据触摸事件参数计算触摸边界宽度,包括以下逻辑: - /// 1. 获取触摸点的边界 - /// 2. 如果不是四边红外屏幕,使用边界宽度 - /// 3. 如果是四边红外屏幕,使用边界宽度和高度的平方根 - /// 4. 如果是特殊屏幕,乘以触摸倍数 - /// 5. 返回计算得到的触摸边界宽度 + /// 手掌擦阈值与特殊屏 TouchMultiplier 在激活逻辑中单独参与计算,此处仅返回几何接触尺寸。 /// public double GetTouchBoundWidth(TouchEventArgs e) { var args = e.GetTouchPoint(null).Bounds; - double value; - if (!Settings.Advanced.IsQuadIR) value = args.Width; - else value = Math.Sqrt(args.Width * args.Height); //四边红外 - if (Settings.Advanced.IsSpecialScreen) value *= Settings.Advanced.TouchMultiplier; - return value; + if (!Settings.Advanced.IsQuadIR) return args.Width; + return Math.Sqrt(args.Width * args.Height); } /// @@ -757,28 +765,146 @@ namespace Ink_Canvas /// private void InkCanvas_PreviewTouchDown(object sender, TouchEventArgs e) { + var touchPointForBar = e.GetTouchPoint(this); + var floatingBarBounds = ViewboxFloatingBar.TransformToAncestor(this).TransformBounds( + new Rect(0, 0, ViewboxFloatingBar.ActualWidth, ViewboxFloatingBar.ActualHeight)); + if (floatingBarBounds.Contains(touchPointForBar.Position)) + return; + + if ((inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint + || inkCanvas.EditingMode == InkCanvasEditingMode.EraseByStroke) + && !isPalmEraserActive) + { + return; + } + + if (drawingShapeMode != 0) + { + inkCanvas.EditingMode = InkCanvasEditingMode.None; + SetCursorBasedOnEditingMode(inkCanvas); + inkCanvas.CaptureTouch(e.TouchDevice); + ViewboxFloatingBar.IsHitTestVisible = false; + BlackboardUIGridForInkReplay.IsHitTestVisible = false; + + isTouchDown = true; + + if (dec.Count == 0) + { + var inkTouchPoint = e.GetTouchPoint(inkCanvas); + if (drawingShapeMode == 24 || drawingShapeMode == 25) + { + if (drawMultiStepShapeCurrentStep == 0) + iniP = inkTouchPoint.Position; + } + else + { + iniP = inkTouchPoint.Position; + } + lastTouchDownStrokeCollection = inkCanvas.Strokes.Clone(); + } + dec.Add(e.TouchDevice.Id); + return; + } + + SetCursorBasedOnEditingMode(inkCanvas); inkCanvas.CaptureTouch(e.TouchDevice); ViewboxFloatingBar.IsHitTestVisible = false; BlackboardUIGridForInkReplay.IsHitTestVisible = false; - + lastTouchDownTime = DateTime.Now; dec.Add(e.TouchDevice.Id); - //设备1个的时候,记录中心点 + + if (Settings.Canvas.EnablePalmEraser && !isPalmEraserActive && drawingShapeMode == 0) + { + var touchPoint = e.GetTouchPoint(inkCanvas); + double boundWidth = GetTouchBoundWidth(e); + + if ((Settings.Advanced.TouchMultiplier != 0 || !Settings.Advanced.IsSpecialScreen) + && (boundWidth > BoundsWidth)) + { + double thresholdMultiplier; + switch (Settings.Canvas.PalmEraserSensitivity) + { + case 0: + thresholdMultiplier = 3.0; + break; + case 1: + thresholdMultiplier = 2.5; + break; + case 2: + default: + thresholdMultiplier = 2.0; + break; + } + + double EraserThresholdValue = Settings.Startup.IsEnableNibMode + ? Settings.Advanced.NibModeBoundsWidthThresholdValue + : Settings.Advanced.FingerModeBoundsWidthThresholdValue; + + if (boundWidth > BoundsWidth * EraserThresholdValue * thresholdMultiplier) + { + boundWidth *= Settings.Startup.IsEnableNibMode + ? Settings.Advanced.NibModeBoundsWidthEraserSize + : Settings.Advanced.FingerModeBoundsWidthEraserSize; + + if (Settings.Advanced.IsSpecialScreen) + boundWidth *= Settings.Advanced.TouchMultiplier; + inkCanvas.EditingMode = InkCanvasEditingMode.EraseByPoint; + isPalmEraserActive = true; + + EnableEraserOverlay(); + eraserWidth = boundWidth; + UpdateEraserStyle(); + touchPoint = e.GetTouchPoint(inkCanvas); + EraserOverlay_PointerDown(sender); + EraserOverlay_PointerMove(sender, touchPoint.Position); + if (Settings.Canvas.IsShowCursor) + { + inkCanvas.ForceCursor = false; + inkCanvas.UseCustomCursor = false; + } + } + } + } + if (dec.Count == 1) { var touchPoint = e.GetTouchPoint(inkCanvas); centerPoint = touchPoint.Position; - - //记录第一根手指点击时的 StrokeCollection lastTouchDownStrokeCollection = inkCanvas.Strokes.Clone(); } - //设备两个及两个以上,将画笔功能关闭 + if (dec.Count > 1 || isSingleFingerDragMode || !Settings.Gesture.IsEnableTwoFingerGesture) { if (isInMultiTouchMode || !Settings.Gesture.IsEnableTwoFingerGesture) return; if (inkCanvas.EditingMode == InkCanvasEditingMode.None || inkCanvas.EditingMode == InkCanvasEditingMode.Select) return; + var timeSinceLastTouch = (DateTime.Now - lastTouchDownTime).TotalMilliseconds; + if (timeSinceLastTouch < MULTI_TOUCH_DELAY_MS && inkCanvas.EditingMode == InkCanvasEditingMode.Ink) + { + if (!isMultiTouchTimerActive) + { + isMultiTouchTimerActive = true; + var remainingTime = MULTI_TOUCH_DELAY_MS - timeSinceLastTouch; + Task.Delay((int)remainingTime).ContinueWith(_ => + { + Dispatcher.Invoke(() => + { + if (dec.Count > 1 && inkCanvas.EditingMode == InkCanvasEditingMode.Ink) + inkCanvas.EditingMode = InkCanvasEditingMode.None; + isMultiTouchTimerActive = false; + }); + }); + } + return; + } + lastInkCanvasEditingMode = inkCanvas.EditingMode; - inkCanvas.EditingMode = InkCanvasEditingMode.None; + if (inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint + && inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke + && drawingShapeMode == 0) + { + inkCanvas.EditingMode = InkCanvasEditingMode.None; + } } } @@ -792,6 +918,11 @@ namespace Ink_Canvas /// private void InkCanvas_PreviewTouchMove(object sender, TouchEventArgs e) { + if (isPalmEraserActive) + { + var touchPoint = e.GetTouchPoint(inkCanvas); + EraserOverlay_PointerMove(sender, touchPoint.Position); + } } /// @@ -818,32 +949,18 @@ namespace Ink_Canvas /// private void InkCanvas_PreviewTouchUp(object sender, TouchEventArgs e) { + if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint && !isPalmEraserActive) + { + return; + } inkCanvas.ReleaseAllTouchCaptures(); ViewboxFloatingBar.IsHitTestVisible = true; BlackboardUIGridForInkReplay.IsHitTestVisible = true; - //手势完成后切回之前的状态 - if (dec.Count > 1) - if (inkCanvas.EditingMode == InkCanvasEditingMode.None) - inkCanvas.EditingMode = lastInkCanvasEditingMode; dec.Remove(e.TouchDevice.Id); - if (dec.Count == 0) - { - isSingleFingerDragMode = false; - isWaitUntilNextTouchDown = false; - if (drawingShapeMode == 0 - && inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint - && inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke - && inkCanvas.EditingMode != InkCanvasEditingMode.Select - && inkCanvas.EditingMode != InkCanvasEditingMode.None) - { - if (lastInkCanvasEditingMode != InkCanvasEditingMode.None) - { - inkCanvas.EditingMode = lastInkCanvasEditingMode; - } - } - } + if (dec.Count <= 1) + isMultiTouchTimerActive = false; if (drawingShapeMode != 0) { @@ -855,12 +972,10 @@ namespace Ink_Canvas { if (drawMultiStepShapeCurrentStep == 0) { - // 第一笔完成,进入第二笔 drawMultiStepShapeCurrentStep = 1; } else { - // 第二笔完成,完成绘制 var mouseArgs = new MouseButtonEventArgs(Mouse.PrimaryDevice, 0, MouseButton.Left) { RoutedEvent = MouseLeftButtonUpEvent, @@ -880,6 +995,36 @@ namespace Ink_Canvas } } + if (drawingShapeMode == 0) + { + if (dec.Count > 1) + { + if (inkCanvas.EditingMode == InkCanvasEditingMode.None) + { + if (lastInkCanvasEditingMode != InkCanvasEditingMode.EraseByPoint) + inkCanvas.EditingMode = lastInkCanvasEditingMode; + } + } + else if (dec.Count == 0) + { + isSingleFingerDragMode = false; + isWaitUntilNextTouchDown = false; + + if (inkCanvas.EditingMode == InkCanvasEditingMode.None && + lastInkCanvasEditingMode != InkCanvasEditingMode.None && + lastInkCanvasEditingMode != InkCanvasEditingMode.EraseByPoint) + { + inkCanvas.EditingMode = lastInkCanvasEditingMode; + } + + if (isPalmEraserActive) + { + isPalmEraserActive = false; + DisableEraserOverlay(); + } + } + } + inkCanvas.Opacity = 1; if (dec.Count == 0) @@ -977,6 +1122,9 @@ namespace Ink_Canvas /// private void Main_Grid_ManipulationDelta(object sender, ManipulationDeltaEventArgs e) { + if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint) + return; + if (isInMultiTouchMode || !Settings.Gesture.IsEnableTwoFingerGesture) return; bool hasMultipleManipulators = e.Manipulators.Count() >= 2; diff --git a/Ink Canvas/MainWindow_cs/MW_TrayIcon.cs b/Ink Canvas/MainWindow_cs/MW_TrayIcon.cs index 302dfac7..76a573c8 100644 --- a/Ink Canvas/MainWindow_cs/MW_TrayIcon.cs +++ b/Ink Canvas/MainWindow_cs/MW_TrayIcon.cs @@ -1,4 +1,4 @@ -using Hardcodet.Wpf.TaskbarNotification; +using H.NotifyIcon; using Ink_Canvas.Helpers; using iNKORE.UI.WPF.Controls; using System; @@ -9,6 +9,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Forms; using System.Windows.Interop; +using System.Windows.Threading; using Application = System.Windows.Application; using ContextMenu = System.Windows.Controls.ContextMenu; using MenuItem = System.Windows.Controls.MenuItem; @@ -17,6 +18,11 @@ namespace Ink_Canvas { public partial class App : Application { + private const int TrayTemporaryShowMinutes = 2; + + private DispatcherTimer _trayTemporaryShowTimer; + + private bool _trayTemporaryShowRestoreHideChecked; /// /// 系统托盘菜单打开时的事件处理方法 @@ -131,6 +137,85 @@ namespace Ink_Canvas } } + private void TempShowMainWindowTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e) + { + var mainWin = Current.MainWindow as MainWindow; + if (mainWin?.IsLoaded != true) + return; + + MenuItem hideItem = null; + try + { + var trayMenu = ((TaskbarIcon)Current.Resources["TaskbarTrayIcon"]).ContextMenu; + hideItem = trayMenu?.Items.OfType() + .FirstOrDefault(mi => mi.Name == "HideICCMainWindowTrayIconMenuItem"); + } + catch + { + } + + _trayTemporaryShowRestoreHideChecked = hideItem?.IsChecked == true; + + EnsureMainWindowReadyForSettings(mainWin); + + global::Ink_Canvas.MainWindow.TrayTemporaryShowUntilUtc = DateTime.UtcNow.AddMinutes(TrayTemporaryShowMinutes); + + _trayTemporaryShowTimer?.Stop(); + if (_trayTemporaryShowTimer == null) + { + _trayTemporaryShowTimer = new DispatcherTimer + { + Interval = TimeSpan.FromMinutes(TrayTemporaryShowMinutes) + }; + _trayTemporaryShowTimer.Tick += TrayTemporaryShowTimer_OnTick; + } + else + { + _trayTemporaryShowTimer.Interval = TimeSpan.FromMinutes(TrayTemporaryShowMinutes); + } + + _trayTemporaryShowTimer.Start(); + } + + private void TrayTemporaryShowTimer_OnTick(object sender, EventArgs e) + { + _trayTemporaryShowTimer?.Stop(); + global::Ink_Canvas.MainWindow.TrayTemporaryShowUntilUtc = null; + + var mainWin = Current.MainWindow as MainWindow; + if (mainWin?.IsLoaded != true) + { + _trayTemporaryShowRestoreHideChecked = false; + return; + } + + try + { + if (_trayTemporaryShowRestoreHideChecked) + { + var trayMenu = ((TaskbarIcon)Current.Resources["TaskbarTrayIcon"]).ContextMenu; + var hideItem = trayMenu?.Items.OfType() + .FirstOrDefault(mi => mi.Name == "HideICCMainWindowTrayIconMenuItem"); + if (hideItem != null) + hideItem.IsChecked = true; + else + mainWin.Hide(); + } + else + { + mainWin.CheckMainWindowVisibility(); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"托盘临时显示计时结束处理失败: {ex.Message}", LogHelper.LogType.Warning); + } + finally + { + _trayTemporaryShowRestoreHideChecked = false; + } + } + private void OpenSettingsTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e) { var mainWin = Current.MainWindow as MainWindow; @@ -326,6 +411,10 @@ namespace Ink_Canvas /// private void HideICCMainWindowTrayIconMenuItem_Checked(object sender, RoutedEventArgs e) { + _trayTemporaryShowTimer?.Stop(); + global::Ink_Canvas.MainWindow.TrayTemporaryShowUntilUtc = null; + _trayTemporaryShowRestoreHideChecked = false; + var mi = (MenuItem)sender; var mainWin = (MainWindow)Current.MainWindow; if (mainWin != null && mainWin.IsLoaded) diff --git a/Ink Canvas/Properties/Strings.Designer.cs b/Ink Canvas/Properties/Strings.Designer.cs index ee51ee34..47c801b1 100644 --- a/Ink Canvas/Properties/Strings.Designer.cs +++ b/Ink Canvas/Properties/Strings.Designer.cs @@ -19,7 +19,7 @@ namespace Ink_Canvas.Properties [CompilerGenerated] public static class Strings { - private const string EmbeddedEnUsResxName = "Ink_Canvas.Properties.Strings.enUS.xml"; + private const string EmbeddedEnUsResxName = "Ink_Canvas.Properties.Strings.en-US.resx"; private static readonly object EnUsLock = new object(); private static Dictionary _embeddedEnUs; private static ResourceManager _resourceMan; @@ -119,5 +119,7 @@ namespace Ink_Canvas.Properties public static string Nav_About => GetString(nameof(Nav_About)) ?? "关于"; public static string App_Title => GetString(nameof(App_Title)) ?? "InkCanvasforClass"; public static string Booth_Resolution_Tooltip => GetString(nameof(Booth_Resolution_Tooltip)) ?? "展台/截图分辨率"; + public static string Tray_TempShowMainWindow => GetString(nameof(Tray_TempShowMainWindow)) ?? "显示主窗口(2分钟)"; + public static string Tray_OpenSettings => GetString(nameof(Tray_OpenSettings)) ?? "打开设置"; } } diff --git a/Ink Canvas/Properties/Strings.en-US.resx b/Ink Canvas/Properties/Strings.en-US.resx index 6ca0619c..36ee52db 100644 --- a/Ink Canvas/Properties/Strings.en-US.resx +++ b/Ink Canvas/Properties/Strings.en-US.resx @@ -1,5 +1,64 @@ + @@ -103,655 +162,2425 @@ Booth / Screenshot resolution - Gesture - Appearance - PPT - Advanced - Automation - Random Picker - Shortcuts - Collapse sidebar - Show sidebar - Control via icc:// protocol - Settings - Changes are saved automatically; some require restart. - Restart - Reset - Exit - Mode - Choose run mode. In PPT-only mode the app is hidden until slide show. (Experimental) - Normal - PPT only - New settings window - Open a new settings window. (In development) - Open new settings - Plugins - Extend Ink Canvas with plugins. Enable, disable, or load custom plugins. - Open plugin manager - Startup - No-focus mode - Borderless - Topmost - UIA topmost - # UIA topmost requires admin to take effect. - Auto-update - Silent update - # Silent update installs when app is idle. - Update channel - Stable (Release) - Preview - Beta - # Stable for reliability; Preview for new features. - Check for updates - # Check and download now. - Version fix - # Download and install latest for current channel. - Rollback - # Open rollback page. - # When silent update is off, you will be prompted after download. - Silent update time range - Start time - End time - # If end < start… - Run at login - Minimize to sidebar at startup - Canvas & ink - Show pen cursor - Pressure-sensitive touch - # Touch devices will support pressure. - Ignore pressure - # Ignore all device pressure. - Eraser size - Very small - Small - Medium - Large - Very large - # Takes effect on next area eraser use. - Hide ink when leaving canvas - # When enabled… - Clear ink history when clearing - Clear images with canvas - Compress images >1920×1080 - Keep hyperbola asymptotes - Yes - No - Ask each time - # Disabling may cause undo bugs. - Show circle center - WPF default Bezier smoothing - Advanced curve smoothing (recommended) - Ink fade - # Ink will not be drawn on canvas when enabled. - Ink fade time - Hide fade in pen menu - # Fade control will be hidden in pen context menu. - Color - Default - Black - White - Red - Yellow - Blue - Green - Orange - Purple - Update downloaded. It will install when you close the app. - Update ready - Update download failed. Please check your network and try again. - Download failed - Version {0} skipped; you will not be prompted until a newer version is released. - Version skipped - An unexpected error occurred. Save your ink and restart the app. - Too many restarts - App has restarted 5 times. Auto-restart stopped. Contact the developer or check the system. - Starting Ink Canvas... - Crash action - Choose what to do when an unhandled exception occurs: - Silent restart - No action - # Silent restart: automatically restart without prompt. No action: only log, do not restart. - Gestures - Auto-toggle two-finger move in/out of whiteboard - # When enabled: leaving canvas disables two-finger move; entering whiteboard enables it. - Allow rotate & scale selected ink - # Allows scaling selected ink with two or more fingers (independent of rotate setting). - Enable palm eraser - Palm eraser sensitivity - Low sensitivity - Medium sensitivity - High sensitivity - # Low: larger area/more touches required (less false positive); High: easier to trigger but may mis-detect fingers. - Ink correction - Enable ink recognition - Block fake pressure on corrected rectangles - Block fake pressure on corrected triangles - Correct freehand triangles - Correct freehand rectangles - Correct circles and ellipses - Auto-straighten lines - Length threshold - Sensitivity - High-precision straightening - # When enabled, lines longer than the threshold will be straightened. Sensitivity 0.05–2.0: smaller = stricter; larger = easier to treat as straight. High-precision samples every 10px for better judgement. - Line endpoint snapping - Snapping distance - Personalization - Theme - Light theme - Dark theme - Follow system - Enable startup animation - Startup animation style - Random - Follow seasons - Spring - Summer - Autumn - Winter - Year-of-Horse special - Floating toolbar icon - “ICC-CE” default - “ICC-CE” no shadow - “ICC-CE” dark - “ICC-CE” dark breathing - “ICC-CE” white transparent - “ICC-CE” black transparent - Coolapk cross-eye emoji - Coolapk abused emoji - Coolapk grin emoji - Coolapk underwear emoji - Coolapk green-hat Doge - Tieba emoji - Custom floating icon - Upload - Manage - Floating toolbar scale - Floating toolbar opacity - Floating bar opacity in PPT - # Takes effect after re-entering slide show - Show nib-mode button in palette - Whiteboard UI 80% scale - Show time and date in whiteboard - Show quotes in whiteboard - Where is the quote from? - osu! player quotes - Inspirational mottos - Gaokao blessings - Hitokoto API - Custom - Enable quick panel in docked mode - Un-dock button icon - Arrow - Pen - Floating bar buttons - Use legacy floating bar UI - Show shape button - Show undo button - Show redo button - Show clear button - Show whiteboard button - Show hide button - Show lasso select button - Show clear+mouse button - Show quick palette - Quick palette display mode - Single row (6 colors) - Double row (8 colors) - Eraser button display - Show both - Area eraser only - Line eraser only - Hide all - Taskbar tray icon - Enable tray icon - PPT integration - These settings apply during slide show and override others. - Microsoft PowerPoint support - PowerPoint enhancement - Steal focus to skip animations (PPT) - Use ROT integration - WPS support - Kill WPP process (avoid leftovers) - # When disabled, leftover WPP processes may cause slow close or cannot exit completely. - # If you only use PowerPoint, do not enable WPS integration. If you use WPS, it is recommended not to use PowerPoint together. - Enabling WPS support may cause lag when closing WPS! - # WPS is supported, but MS Office and WPS cannot be supported at the same time. To enable WPS support, make sure “WPS Office compatibility with third-party systems and software” is enabled in the WPS config tool, otherwise WPS cannot be detected. - Hide ink when exiting board mode - # When this option is on, ink will not be shown in PPT mode if not in annotation mode. - Clear ink history when clearing ink - Clear images when clearing canvas - Auto-compress images when inserting (larger than 1920x1080) - PPT page-turn buttons - Show page-turn buttons in PPT mode - Bottom left - Bottom right - Left - Right - Left offset - Left opacity - Right offset - Right opacity - # Increase for up, decrease for down; 0 = no offset, centered. - Bottom left offset - Bottom left opacity - Bottom right offset - Bottom right opacity - # Increase for right, decrease for left; 0 = no offset, centered. - Sides - Show page number - Half opacity - Black background - Bottom left & right - PPT page button clickable - # When enabled, clicking the page button opens PowerPoint grid thumbnails. Not supported in WPS. - PPT long-press to turn page - # When enabled, long-press on PPT page button to turn pages continuously. - # With UIA topmost on, app needs admin to stay on top. To turn off, fully quit then start again; restart will not disable it. - Silent update - # Silent update installs when the app is idle; no manual action needed. - Update channel - Stable (Release) - Preview - Beta - # Stable: reliable updates. Preview: new features with better stability than Beta. Beta: earliest new features. - Check for updates - # Check and download the latest version now. - Repair installation - # Repair downloads the latest build for the selected channel and reinstalls; use to fix broken installs. - Rollback to previous version - # Opens a page to manually roll back to an earlier version. - # When silent update is off, you will be prompted after download. When on, every 10 minutes the app checks: 1) within silent-update time window 2) not in writing mode 3) not in canvas. If all pass, it will close and update. - Silent update time window - Start time - End time - # If end < start, end is next day. If start = end, window is 24h. - Run at startup - Dock to sidebar after startup - Canvas and ink - Show pen cursor - Enable pressure-sensitive touch - # When on, touch screens that support pressure will show pressure; for devices not recognized by the system. - Ignore pressure - # When on, all strokes use uniform thickness; mutually exclusive with pressure-sensitive touch. - Eraser size - Very small - Small - Medium - Large - Very large - # Change takes effect next time you use area eraser. - Keep hyperbola asymptotes - Yes - No - Ask each time - # If not kept, undo-related bugs may occur. - Show circle center when drawing - Use WPF default Bezier smoothing - Use advanced curve smoothing (recommended) - Enable ink fade - # When on, ink is not committed to canvas; it fades after the set time. - Ink fade time - Hide ink fade control in pen menu - # When on, the pen context menu will not show the ink fade control. - Enable brush auto-restore - # When on, temporary brush changes will restore at the configured time(s) to the color/opacity/width set here. - Auto-restore time points (HH:mm, multiple with ;) - Restore color - Default - Black - White - Red - Yellow - Blue - Green - Orange - Purple - Restore stroke width - Restore opacity - Switch back to annotation after eraser - # When on, after erasing, staying idle for a while will switch back to annotation mode. - Auto switch delay - # If you erase again within the delay, the timer resets. - # When on, line endpoints near other endpoints will snap and connect. - Enter annotation mode when starting PPT slide show - Conflicts with "Auto fold when playing PPT" in Automation! - Allow two-finger gestures in slide show - Allow finger gesture to turn slides - # When canvas is on, finger swipe (not pen) can turn slides in show mode when canvas has no ink. - Show gesture buttons in PPT slide show - # When on, gesture buttons are shown in PPT slide show. - PPT time capsule - # When on, show time capsule in PPT show; can replace minimized timer window. - Time capsule position: - Top left - Top right - Top center - Show quick panel in PPT slide show - # When off, quick panel is hidden in PPT slide show. - Auto screenshot on slide change - # When on, auto-screenshot when turning page with ink on slide. - Auto-save slide ink - # When on, ink is saved when ending slide show and loaded next time (same file and page). - Remember and prompt last slide position - # When on, last page is recorded; choose Yes to jump to it. - Go to first slide when entering show - Warn about hidden slides - Warn if auto-play is enabled - Advanced - Adjust when finger-touch shows circle eraser or palm eraser is much larger than palm - Special screen mode - Touch multiplier - Tap with pen in the area below to estimate touch size multiplier - # Value is for reference only - Bind eraser to touch size multiplier - # BoundsWidth is used as contact area threshold - Quad IR mode - Enable logging - Save logs by date - # Log files over 512 KB are auto-deleted. With date save, logs go to Logs folder; folder is cleared when over 5 MB. - Confirm exit with dialog - Enable FullScreenHelper - Experimental - # Thanks to lindexi for FullScreenHelper; reduces taskbar pop-up and supports multi-monitor fullscreen. Disable if you see odd issues; restart ICC to apply. - Enable AvoidFullScreenHelper - # Avoid canvas fullscreen; may fix taskbar not on top and Win11 taskbar unclickable. Can cause floating bar offset with AppBar on left/top. Restart ICC to apply. - Enable EdgeGestureUtil - More features - Timer - Random draw - Single draw - Save - Open... - Replay - Screenshot - Manual - Settings - Single draw - Random draw - Timer - Whiteboard - Exit slide show - Show - Exit - Settings backup & restore - # You can manually back up current settings or restore previous backups; backups are also created automatically before updates. - Backup before update - Periodic auto-backup - Backup interval - 1 day - 3 days - 7 days - 14 days - 30 days - (default: 7 days) - Backup now - Restore backup - Config profiles & hot reload - # Selecting a profile switches and hot-reloads it; \"Save as\" saves current settings as a new profile. - Profile: - Delete profile - Save as profile - Automation - Auto fold - Seewo Whiteboard 5 - Seewo Visual Presenter - Seewo Whiteboard 3 - Seewo Lite Whiteboard - Seewo Lite Whiteboard 5C - Seewo Pinco - HiteBoard - Hite visual presenter - Hite Lite Whiteboard - WenXiang Whiteboard - Microsoft Whiteboard - Admox Whiteboard - Admox visual presenter - YiYun Whiteboard - YiYun visual presenter - MaxHub Whiteboard - Ignore EN5 desktop annotation window when auto folding - Auto fold when entering old ZhongYuan whiteboard - Auto fold while playing PPT - Keep folded after app exit - # When on, apps that trigger auto fold will stay folded even after they exit. - Auto kill - Auto kill Seewo PPT tools - # Killing PPT tools disables Seewo classroom helper. Delete Office.dll in its install folder to stop the PPT toolbar without auto kill. - Auto kill Seewo Whiteboard 5 - Auto kill Hite screen writing - Enter annotation after killing Hite screen writing - Auto kill YouJiao teacher - Auto kill Seewo Desktop 2.0 annotation - # Seewo Desktop 2.0 annotation is 64-bit so ICC (32-bit) cannot inspect it deeply; only process name DesktopAnnotation is matched. If you have another app with the same name, keep this off. - Kill similar apps - Auto kill Ink Canvas and IC+ - Auto kill ICA (both new & old) - Auto kill Inkeys (new only) - File association - Manage .icstk file association so double-click opens in Ink Canvas. - Remove association - Check status - Register association - Floating window interceptor - Detect and block floating windows from similar software - Enable floating window interceptor - Interceptor not running - Auto screenshot on clear - Save screenshots in date folders - Auto-save ink when screenshotting - Auto-save ink periodically - Save interval - 1 minute - 3 minutes - 5 minutes - 10 minutes - 15 minutes - 30 minutes - 60 minutes - # When on, strokes are auto-saved at the set interval, only when canvas is visible and has ink. - Save full-page strokes - # When on, auto/manual saves store all pages in fullscreen; multiple pages are packed in one archive (whiteboard strokes open only in whiteboard mode; PPT strokes only in slide show mode). - Save as XML format - # When on, strokes are saved as XML (ISF) for easier inspection and editing. - Minimum ink for auto screenshot - Stroke and screenshot save path - Browse - Set save path to D:\Ink Canvas - Set save path to Documents - # Please ensure the save folder is writable. - Auto delete old strokes and screenshots - # When on, all .icstk and .png files in the auto-save folder may be deleted! - Retention duration - days - Cloud storage management - Fold mode - Switch to annotation when exiting fold mode - # When on, exiting fold mode switches back to annotation for convenience. - Auto fold floating bar after PPT show - # When on, floating bar is auto-folded after exiting PPT slide show. - Auto fold when exiting whiteboard - # When on, exiting whiteboard folds back to sidebar. - Random roll call - Show button to edit name list - Roll-call window background (legacy UI only) - Background: - Default background - Custom background: - Upload - Manage - Enable random & single-draw buttons - Enable quick-draw floating button - Use external roll-call app - Roll-call type - ClassIsland - SecRandom - NamePicker - Single-draw window close delay - Max students per single draw - New roll-call UI - Enable new roll-call UI - Use machine learning to avoid repeats - History count for avoidance - Avoidance weight - # ML analyzes recent roll-call history to avoid repeating the same students. - Timer settings - Use legacy timer button UI - New timer UI - Enable count-up after timeout - Highlight numbers when overtime - Timer alert volume - Custom alert sound: - Select file - Reset - Progressive reminder - Progressive reminder volume - Custom progressive reminder audio: - Select file - Reset - About - Device information - Device ID: - Usage frequency: - Update priority: - Launch count: - Total usage time: - Loading... - Refresh device info - I have read and agree to the - privacy statement - Anonymous usage data upload: - Off (no upload) - Upload basic data - Upload basic + optional data - # Before using or distributing this software, you must be aware of the related open-source licenses. This software is based on https://github.com/WXRIW/Ink-Canvas. - This software, ICA and Ink Canvas are all open sourced under a license - The strong copyleft license requires that complete source code and modifications of the licensed work (including large works using it) be provided under the same license. Copyright and license notices must be retained. Contributors explicitly grant patent rights. - Developers: - Developer of ICC CE - Developer of ICC - Developer of ICA - Developer of Ink Canvas - ICC repository: - ICA repository: - Ink Canvas repository: - Thanks to the following contributors: - © 2025-2026 CJK_mkp. All rights reserved. - We love open-source forever! - Version: - Close - On - Off - External URI scheme (icc://) - Nib mode BoundsWidth - Finger mode BoundsWidth - # EdgeGestureUtil is newly introduced in ICC to temporarily block edge gestures when using touch (e.g., on Windows 10: swipe from the left edge to Task View, from the right edge to Action Center; on Windows 11: swipe up from the bottom to open Start). It works by using - (When the app window is active and in full-screen mode (or an owned window is active), prevents edge gesture behavior.) If anything is abnormal, turn this option off; it should take effect immediately. (Not available on Windows 7/8.) - Enable ForceFullScreen - # When a window size change is detected, automatically uses Win32 API to set this window size to the primary monitor size (in device pixels). Turn it off if you don't need it; takes effect immediately. - Enable DPIChangeDetection - # When a system DPI change is detected, it tries to keep FloatingBar visible. If it goes off-screen, it will attempt to move it into the visible area. (Increasing DPI may trigger this; decreasing DPI won't auto-move—adjust manually.) - Enable ResolutionChangeDetection - # When a screen resolution change is detected, it tries to keep FloatingBar visible. If it goes off-screen, it will attempt to move it into the visible area. (Reducing resolution may trigger this; if it's still on-screen it won't auto-adjust—adjust manually.) - Seewo Whiteboard 3 - Seewo Whiteboard 5 - Seewo Whiteboard 5C - Seewo Pinco - Seewo Pinco pen - Seewo PPT Tools - AiClass - Hite screen writing - Changyan smart classroom - Changyan PPT - Tianyu Education Cloud - Seewo desktop pen - Seewo desktop sidebar - Multi-touch writing - Two-finger move - Two-finger zoom - Two-finger rotate - Background - Select - Pen - Highlighter - Eraser - Eraser options - Size - Eraser shape - Circle - Blackboard - Clear ink - Clear ink & history - Stroke eraser - Shapes - (Long-press in first row to keep selected) - Auto-hide - Insert image - Select image - Screenshot - Undo - Redo - Tools - Exit - New page - Previous - Next - Page - Delete this page - Test text - Exit - Thickness - Dark - Background - Hide canvas - Check - From start slideshow - End slideshow - One finger drag - Restore - Clear & Hide - Mouse - Annotate - Clear - Visual presenter - Captured photos - Camera devices - Present - Correct - Capture - Rotate - UI language - Follow system - Chinese (Simplified) - English - You need to restart the app for language changes to fully take effect. - Area eraser - Stroke eraser - Lasso - Geometry - Clear & cursor - Board - Hide - Geometry drawing - Line - Dashed line - Dotted line - Arrow - 4 parallel lines - Centered square - Centered circle - Centered dashed circle - Centered ellipse - Cuboid - Square - Cylinder - Cone - Gesture - Gesture options - Multi-touch writing - Two-finger move - Two-finger zoom - Two-finger rotate - Gesture - Gesture options - + + Gesture + + + Appearance + + + PPT + + + Advanced + + + Automation + + + Random Picker + + + Shortcuts + + + Collapse sidebar + + + Show sidebar + + + Control via icc:// protocol + + + Settings + + + Changes are saved automatically; some require restart. + + + Restart + + + Reset + + + Exit + + + Mode + + + Choose run mode. In PPT-only mode the app is hidden until slide show. (Experimental) + + + Run mode + + + Normal + + + PPT only + + + New settings window + + + Open a new settings window. (In development) + + + Open new settings + + + Plugins + + + Extend Ink Canvas with plugins. Enable, disable, or load custom plugins. + + + Open plugin manager + + + Startup + + + No-focus mode + + + Borderless + + + Topmost + + + UIA topmost + + + # UIA topmost requires admin to take effect. + + + Auto-update + + + Silent update + + + # Silent update installs when app is idle. + + + Update channel + + + Stable (Release) + + + Preview + + + Beta + + + # Stable for reliability; Preview for new features. + + + Check for updates + + + # Check and download now. + + + Version fix + + + # Download and install latest for current channel. + + + Rollback + + + # Open rollback page. + + + # When silent update is off, you will be prompted after download. + + + Silent update time range + + + Start time + + + End time + + + # If end < start… + + + Run at login + + + Minimize to sidebar at startup + + + Canvas & ink + + + Show pen cursor + + + Pressure-sensitive touch + + + # Touch devices will support pressure. + + + Ignore pressure + + + # Ignore all device pressure. + + + Eraser size + + + Very small + + + Small + + + Medium + + + Large + + + Very large + + + # Takes effect on next area eraser use. + + + Hide ink when leaving canvas + + + # When enabled… + + + Clear ink history when clearing + + + Clear images with canvas + + + Compress images >1920×1080 + + + Keep hyperbola asymptotes + + + Yes + + + No + + + Ask each time + + + # Disabling may cause undo bugs. + + + Show circle center when drawing + + + WPF default Bezier smoothing + + + Advanced curve smoothing (recommended) + + + Ink fade + + + # Ink will not be drawn on canvas when enabled. + + + Ink fade time + + + Hide fade in pen menu + + + # Fade control will be hidden in pen context menu. + + + Color + + + Default + + + Black + + + White + + + Red + + + Yellow + + + Blue + + + Green + + + Orange + + + Purple + + + Update downloaded. It will install when you close the app. + + + Update ready + + + Update download failed. Please check your network and try again. + + + Download failed + + + Version {0} skipped; you will not be prompted until a newer version is released. + + + Version skipped + + + An unexpected error occurred. Save your ink and restart the app. + + + Too many restarts + + + App has restarted 5 times. Auto-restart stopped. Contact the developer or check the system. + + + Starting Ink Canvas... + + + Crash action + + + Choose what to do when an unhandled exception occurs: + + + Silent restart + + + No action + + + # Silent restart: automatically restart without prompt. No action: only log, do not restart. + + + Gestures + + + Auto-toggle two-finger move in/out of whiteboard + + + # When enabled: leaving canvas disables two-finger move; entering whiteboard enables it. + + + Allow rotate & scale selected ink + + + # Allows scaling selected ink with two or more fingers (independent of rotate setting). + + + Enable palm eraser + + + Palm eraser sensitivity + + + Low sensitivity + + + Medium sensitivity + + + High sensitivity + + + # Low: larger area/more touches required (less false positive); High: easier to trigger but may mis-detect fingers. + + + Ink correction + + + Enable ink recognition + + + Block fake pressure on corrected rectangles + + + Block fake pressure on corrected triangles + + + Correct freehand triangles + + + Correct freehand rectangles + + + Correct circles and ellipses + + + Auto-straighten lines + + + Length threshold + + + Sensitivity + + + High-precision straightening + + + # When enabled, lines longer than the threshold will be straightened. Sensitivity 0.05–2.0: smaller = stricter; larger = easier to treat as straight. High-precision samples every 10px for better judgement. + + + Line endpoint snapping + + + Snapping distance + + + Personalization + + + Theme + + + Light theme + + + Dark theme + + + Follow system + + + Enable startup animation + + + Startup animation style + + + Random + + + Follow seasons + + + Spring + + + Summer + + + Autumn + + + Winter + + + Year-of-Horse special + + + Floating toolbar icon + + + “ICC-CE” default + + + “ICC-CE” no shadow + + + “ICC-CE” dark + + + “ICC-CE” dark breathing + + + “ICC-CE” white transparent + + + “ICC-CE” black transparent + + + Coolapk cross-eye emoji + + + Coolapk abused emoji + + + Coolapk grin emoji + + + Coolapk underwear emoji + + + Coolapk green-hat Doge + + + Tieba emoji + + + Custom floating icon + + + Upload + + + Manage + + + Floating toolbar scale + + + Floating toolbar opacity + + + Floating bar opacity in PPT + + + # Takes effect after re-entering slide show + + + Show nib-mode button in palette + + + Whiteboard UI 80% scale + + + Show time and date in whiteboard + + + Show quotes in whiteboard + + + Where is the quote from? + + + osu! player quotes + + + Inspirational mottos + + + Gaokao blessings + + + Hitokoto API + + + Custom + + + Enable quick panel in docked mode + + + Un-dock button icon + + + Arrow + + + Pen + + + Floating bar buttons + + + Use legacy floating bar UI + + + Show shape button + + + Show undo button + + + Show redo button + + + Show clear button + + + Show whiteboard button + + + Show hide button + + + Show lasso select button + + + Show clear+mouse button + + + Show quick palette + + + Quick palette display mode + + + Single row (6 colors) + + + Double row (8 colors) + + + Eraser button display + + + Show both + + + Area eraser only + + + Line eraser only + + + Hide all + + + Taskbar tray icon + + + Enable tray icon + + + PPT integration + + + These settings apply during slide show and override others. + + + Microsoft PowerPoint support + + + PowerPoint enhancement + + + Steal focus to skip animations (PPT) + + + Use ROT integration + + + WPS support + + + Kill WPP process (avoid leftovers) + + + # When disabled, leftover WPP processes may cause slow close or cannot exit completely. + + + # If you only use PowerPoint, do not enable WPS integration. If you use WPS, it is recommended not to use PowerPoint together. + + + Enabling WPS support may cause lag when closing WPS! + + + # WPS is supported, but MS Office and WPS cannot be supported at the same time. To enable WPS support, make sure “WPS Office compatibility with third-party systems and software” is enabled in the WPS config tool, otherwise WPS cannot be detected. + + + Hide ink when exiting board mode + + + # When this option is on, ink will not be shown in PPT mode if not in annotation mode. + + + Clear ink history when clearing ink + + + Clear images when clearing canvas + + + Auto-compress images when inserting (larger than 1920x1080) + + + PPT page-turn buttons + + + Show page-turn buttons in PPT mode + + + Bottom left + + + Bottom right + + + Left + + + Right + + + Left offset + + + Left opacity + + + Right offset + + + Right opacity + + + # Increase for up, decrease for down; 0 = no offset, centered. + + + Bottom left offset + + + Bottom left opacity + + + Bottom right offset + + + Bottom right opacity + + + # Increase for right, decrease for left; 0 = no offset, centered. + + + Sides + + + Show page number + + + Half opacity + + + Black background + + + Bottom left & right + + + PPT page button clickable + + + # When enabled, clicking the page button opens PowerPoint grid thumbnails. Not supported in WPS. + + + PPT long-press to turn page + + + # When enabled, long-press on PPT page button to turn pages continuously. + + + # With UIA topmost on, app needs admin to stay on top. To turn off, fully quit then start again; restart will not disable it. + + + # Silent update installs when the app is idle; no manual action needed. + + + Update channel + + + Stable (Release) + + + Preview + + + Beta + + + # Stable: reliable updates. Preview: new features with better stability than Beta. Beta: earliest new features. + + + # Check and download the latest version now. + + + Repair installation + + + # Repair downloads the latest build for the selected channel and reinstalls; use to fix broken installs. + + + Rollback to previous version + + + # Opens a page to manually roll back to an earlier version. + + + # When silent update is off, you will be prompted after download. When on, every 10 minutes the app checks: 1) within silent-update time window 2) not in writing mode 3) not in canvas. If all pass, it will close and update. + + + Silent update time window + + + Start time + + + End time + + + # If end < start, end is next day. If start = end, window is 24h. + + + Run at startup + + + Dock to sidebar after startup + + + Canvas and ink + + + Show pen cursor + + + Enable pressure-sensitive touch + + + # When on, touch screens that support pressure will show pressure; for devices not recognized by the system. + + + Ignore pressure + + + # When on, all strokes use uniform thickness; mutually exclusive with pressure-sensitive touch. + + + Very small + + + Small + + + Medium + + + Large + + + Very large + + + # Change takes effect next time you use area eraser. + + + Keep hyperbola asymptotes + + + Yes + + + No + + + Ask each time + + + # If not kept, undo-related bugs may occur. + + + Use WPF default Bezier smoothing + + + Use advanced curve smoothing (recommended) + + + Enable ink fade + + + # When on, ink is not committed to canvas; it fades after the set time. + + + Hide ink fade control in pen menu + + + # When on, the pen context menu will not show the ink fade control. + + + Enable brush auto-restore + + + # When on, temporary brush changes will restore at the configured time(s) to the color/opacity/width set here. + + + Auto-restore time points (HH:mm, multiple with ;) + + + Restore color + + + Default + + + Black + + + White + + + Red + + + Yellow + + + Blue + + + Green + + + Orange + + + Purple + + + Restore stroke width + + + Restore opacity + + + Switch back to annotation after eraser + + + # When on, after erasing, staying idle for a while will switch back to annotation mode. + + + Auto switch delay + + + # If you erase again within the delay, the timer resets. + + + # When on, line endpoints near other endpoints will snap and connect. + + + Enter annotation mode when starting PPT slide show + + + Conflicts with "Auto fold when playing PPT" in Automation! + + + Allow two-finger gestures in slide show + + + Allow finger gesture to turn slides + + + # When canvas is on, finger swipe (not pen) can turn slides in show mode when canvas has no ink. + + + Show gesture buttons in PPT slide show + + + # When on, gesture buttons are shown in PPT slide show. + + + PPT time capsule + + + # When on, show time capsule in PPT show; can replace minimized timer window. + + + Time capsule position: + + + Top left + + + Top right + + + Top center + + + Show quick panel in PPT slide show + + + # When off, quick panel is hidden in PPT slide show. + + + Auto screenshot on slide change + + + # When on, auto-screenshot when turning page with ink on slide. + + + Auto-save slide ink + + + # When on, ink is saved when ending slide show and loaded next time (same file and page). + + + Remember and prompt last slide position + + + # When on, last page is recorded; choose Yes to jump to it. + + + Go to first slide when entering show + + + Warn about hidden slides + + + Warn if auto-play is enabled + + + Advanced + + + Adjust when finger-touch shows circle eraser or palm eraser is much larger than palm + + + Special screen mode + + + Touch multiplier + + + Tap with pen in the area below to estimate touch size multiplier + + + # Value is for reference only + + + Bind eraser to touch size multiplier + + + # BoundsWidth is used as contact area threshold + + + Quad IR mode + + + Enable logging + + + Save logs by date + + + # Log files over 512 KB are auto-deleted. With date save, logs go to Logs folder; folder is cleared when over 5 MB. + + + Confirm exit with dialog + + + Enable FullScreenHelper + + + Experimental + + + # Thanks to lindexi for FullScreenHelper; reduces taskbar pop-up and supports multi-monitor fullscreen. Disable if you see odd issues; restart ICC to apply. + + + Enable AvoidFullScreenHelper + + + # Avoid canvas fullscreen; may fix taskbar not on top and Win11 taskbar unclickable. Can cause floating bar offset with AppBar on left/top. Restart ICC to apply. + + + Enable EdgeGestureUtil + + + More features + + + Timer + + + Random draw + + + Single draw + + + Save + + + Open... + + + Replay + + + Screenshot + + + Manual + + + Settings + + + Single draw + + + Random draw + + + Timer + + + Whiteboard + + + Exit slide show + + + Annotation bar + + + Exit + + + Settings backup & restore + + + # You can manually back up current settings or restore previous backups; backups are also created automatically before updates. + + + Backup before update + + + Periodic auto-backup + + + Backup interval + + + 1 day + + + 3 days + + + 7 days + + + 14 days + + + 30 days + + + (default: 7 days) + + + Backup now + + + Restore backup + + + Config profiles & hot reload + + + # Selecting a profile switches and hot-reloads it; \"Save as\" saves current settings as a new profile. + + + Profile: + + + Delete profile + + + Save as profile + + + Automation + + + Auto fold + + + Seewo Whiteboard 5 + + + Seewo Visual Presenter + + + Seewo Whiteboard 3 + + + Seewo Lite Whiteboard + + + Seewo Lite Whiteboard 5C + + + Seewo Pinco + + + HiteBoard + + + Hite visual presenter + + + Hite Lite Whiteboard + + + WenXiang Whiteboard + + + Microsoft Whiteboard + + + Admox Whiteboard + + + Admox visual presenter + + + YiYun Whiteboard + + + YiYun visual presenter + + + MaxHub Whiteboard + + + Ignore EN5 desktop annotation window when auto folding + + + Auto fold when entering old ZhongYuan whiteboard + + + Auto fold while playing PPT + + + Keep folded after app exit + + + # When on, apps that trigger auto fold will stay folded even after they exit. + + + Auto kill + + + Auto kill Seewo PPT tools + + + # Killing PPT tools disables Seewo classroom helper. Delete Office.dll in its install folder to stop the PPT toolbar without auto kill. + + + Auto kill Seewo Whiteboard 5 + + + Auto kill Hite screen writing + + + Enter annotation after killing Hite screen writing + + + Auto kill YouJiao teacher + + + Auto kill Seewo Desktop 2.0 annotation + + + # Seewo Desktop 2.0 annotation is 64-bit so ICC (32-bit) cannot inspect it deeply; only process name DesktopAnnotation is matched. If you have another app with the same name, keep this off. + + + Kill similar apps + + + Auto kill Ink Canvas and IC+ + + + Auto kill ICA (both new & old) + + + Auto kill Inkeys (new only) + + + File association + + + Manage .icstk file association so double-click opens in Ink Canvas. + + + Remove association + + + Check status + + + Register association + + + Floating window interceptor + + + Detect and block floating windows from similar software + + + Enable floating window interceptor + + + Interceptor not running + + + Auto screenshot on clear + + + Save screenshots in date folders + + + Auto-save ink when screenshotting + + + Auto-save ink periodically + + + Save interval + + + 1 minute + + + 3 minutes + + + 5 minutes + + + 10 minutes + + + 15 minutes + + + 30 minutes + + + 60 minutes + + + # When on, strokes are auto-saved at the set interval, only when canvas is visible and has ink. + + + Save full-page strokes + + + # When on, auto/manual saves store all pages in fullscreen; multiple pages are packed in one archive (whiteboard strokes open only in whiteboard mode; PPT strokes only in slide show mode). + + + Save as XML format + + + # When on, strokes are saved as XML (ISF) for easier inspection and editing. + + + Minimum ink for auto screenshot + + + Stroke and screenshot save path + + + Browse + + + Set save path to D:\Ink Canvas + + + Set save path to Documents + + + # Please ensure the save folder is writable. + + + Auto delete old strokes and screenshots + + + # When on, all .icstk and .png files in the auto-save folder may be deleted! + + + Retention duration + + + days + + + Cloud storage management + + + Fold mode + + + Switch to annotation when exiting fold mode + + + # When on, exiting fold mode switches back to annotation for convenience. + + + Auto fold floating bar after PPT show + + + # When on, floating bar is auto-folded after exiting PPT slide show. + + + Auto fold when exiting whiteboard + + + # When on, exiting whiteboard folds back to sidebar. + + + Random roll call + + + Show button to edit name list + + + Roll-call window background (legacy UI only) + + + Background: + + + Default background + + + Custom background: + + + Upload + + + Manage + + + Enable random & single-draw buttons + + + Enable quick-draw floating button + + + Use external roll-call app + + + Roll-call type + + + ClassIsland + + + SecRandom + + + NamePicker + + + Single-draw window close delay + + + Max students per single draw + + + New roll-call UI + + + Enable new roll-call UI + + + Use machine learning to avoid repeats + + + History count for avoidance + + + Avoidance weight + + + # ML analyzes recent roll-call history to avoid repeating the same students. + + + Timer settings + + + Use legacy timer button UI + + + New timer UI + + + Enable count-up after timeout + + + Highlight numbers when overtime + + + Timer alert volume + + + Custom alert sound: + + + Select file + + + Reset + + + Progressive reminder + + + Progressive reminder volume + + + Custom progressive reminder audio: + + + Select file + + + Reset + + + About + + + Device information + + + Device ID: + + + Usage frequency: + + + Update priority: + + + Launch count: + + + Total usage time: + + + Loading... + + + Refresh device info + + + I have read and agree to the + + + privacy statement + + + Anonymous usage data upload: + + + Off (no upload) + + + Upload basic data + + + Upload basic + optional data + + + # Before using or distributing this software, you must be aware of the related open-source licenses. This software is based on https://github.com/WXRIW/Ink-Canvas. + + + This software, ICA and Ink Canvas are all open sourced under a license + + + The strong copyleft license requires that complete source code and modifications of the licensed work (including large works using it) be provided under the same license. Copyright and license notices must be retained. Contributors explicitly grant patent rights. + + + Developers: + + + Developer of ICC CE + + + Developer of ICC + + + Developer of ICA + + + Developer of Ink Canvas + + + ICC repository: + + + ICA repository: + + + Ink Canvas repository: + + + Thanks to the following contributors: + + + © 2025-2026 CJK_mkp. All rights reserved. + + + We love open-source forever! + + + Version: + + + Close + + + On + + + Off + + + External URI scheme (icc://) + + + Nib mode BoundsWidth + + + Finger mode BoundsWidth + + + # EdgeGestureUtil is newly introduced in ICC to temporarily block edge gestures when using touch (e.g., on Windows 10: swipe from the left edge to Task View, from the right edge to Action Center; on Windows 11: swipe up from the bottom to open Start). It works by using + + + (When the app window is active and in full-screen mode (or an owned window is active), prevents edge gesture behavior.) If anything is abnormal, turn this option off; it should take effect immediately. (Not available on Windows 7/8.) + + + Enable ForceFullScreen + + + # When a window size change is detected, automatically uses Win32 API to set this window size to the primary monitor size (in device pixels). Turn it off if you don't need it; takes effect immediately. + + + Enable DPIChangeDetection + + + # When a system DPI change is detected, it tries to keep FloatingBar visible. If it goes off-screen, it will attempt to move it into the visible area. (Increasing DPI may trigger this; decreasing DPI won't auto-move—adjust manually.) + + + Enable ResolutionChangeDetection + + + # When a screen resolution change is detected, it tries to keep FloatingBar visible. If it goes off-screen, it will attempt to move it into the visible area. (Reducing resolution may trigger this; if it's still on-screen it won't auto-adjust—adjust manually.) + + + Seewo Whiteboard 3 + + + Seewo Whiteboard 5 + + + Seewo Whiteboard 5C + + + Seewo Pinco + + + Seewo Pinco pen + + + Seewo PPT Tools + + + AiClass + + + Hite screen writing + + + Changyan smart classroom + + + Changyan PPT + + + Tianyu Education Cloud + + + Seewo desktop pen + + + Seewo desktop sidebar + + + Multi-touch writing + + + Two-finger move + + + Two-finger zoom + + + Two-finger rotate + + + Background + + + Select + + + Pen + + + Highlighter + + + Eraser + + + Eraser options + + + Size + + + Eraser shape + + + Circle + + + Blackboard + + + Clear ink + + + Clear ink & history + + + Stroke eraser + + + Shapes + + + (Long-press in first row to keep selected) + + + Auto-hide + + + Insert image + + + Select image + + + Screenshot + + + Undo + + + Redo + + + Tools + + + Exit + + + New page + + + Previous + + + Next + + + Page + + + Delete this page + + + Test text + + + Exit + + + Thickness + + + Dark + + + Background + + + Hide +canvas + + + Check + + + From start +slideshow + + + End +slideshow + + + One finger +drag + + + Restore + + + Clear +& +Hide + + + Mouse + + + Annotate + + + Clear + + + Visual presenter + + + Captured photos + + + Camera devices + + + Present + + + Correct + + + Capture + + + Rotate + + + UI language + + + Follow system + + + Chinese (Simplified) + + + English + + + You need to restart the app for language changes to fully take effect. + + + Area eraser + + + Stroke eraser + + + Lasso + + + Geometry + + + Clear & cursor + + + Board + + + Hide + + + Geometry drawing + + + Line + + + Dashed line + + + Dotted line + + + Arrow + + + 4 parallel lines + + + Centered square + + + Centered circle + + + Centered dashed circle + + + Centered ellipse + + + Cuboid + + + Square + + + Cylinder + + + Cone + + + Gesture + + + Gesture options + + + Multi-touch writing + + + Two-finger move + + + Two-finger zoom + + + Two-finger rotate + + + Gesture + + + Gesture options + + + Still receive input when window loses focus + + + Hide window border for a more immersive experience + + + Window always stays on top of other windows + + + Automatically run the software when system starts + + + Automatically dock to sidebar after startup + + + When enabled, the software will be completely hidden and only appear during PPT slideshow + + + Enable special screen mode to optimize touch experience + + + Adjust touch size multiplier + + + Bind eraser size to touch size multiplier + + + Nib mode contact area threshold + + + Finger mode contact area threshold + + + Enable quad IR touch screen mode + + + Enable logging function + + + Show confirmation dialog when closing software + + + Auto backup current settings before auto update + + + Regularly auto backup settings file + + + Set interval for regular backup + + + After enabling, can be controlled via commands like icc://fold or icc://unfold + + + After enabling, automatically detect and intercept floating windows of similar software + + + Select application theme style + + + Show animation effect at startup + + + Select startup animation style + + + Select floating toolbar icon style + + + Adjust floating toolbar size + + + Adjust floating toolbar opacity + + + Floating toolbar opacity in PPT mode (effective after re-entering PPT slideshow) + + + Show nib mode toggle button in color palette window + + + Use legacy floating bar button UI style + + + Show shape button in floating bar + + + Show undo button in floating bar + + + Show redo button in floating bar + + + Show clear button in floating bar + + + Show whiteboard button in floating bar + + + Show hide button in floating bar + + + Show lasso select button in floating bar + + + Show clear and mouse button in floating bar + + + Show quick color palette in floating bar + + + Select quick color palette display mode + + + Show icon in taskbar system tray + + + Scale whiteboard interface to 80% + + + Show current time and date in whiteboard interface + + + Show inspirational quotes in whiteboard interface + + + Show quick operation panel in folded mode + + + Select unfold button icon style + + + When enabled, automatically switch to annotation mode when exiting fold mode for quick annotation + + + When enabled, the floating bar will automatically fold after exiting PPT slideshow + + + When enabled, will automatically dock to sidebar when exiting whiteboard mode + + + When enabled, software with auto fold will remain folded after software exit + + + Auto enter annotation mode after killing Hite screen writing + + + Show pen cursor when drawing + + + When enabled, touch screen devices will also support pressure sensitivity, suitable for some touch screens that support pressure but cannot be recognized by the system + + + When enabled, will ignore pressure information from all devices, making all strokes uniform thickness. Mutually exclusive with pressure touch mode + + + When enabled, ink will not be displayed when entering PPT mode without being in annotation mode + + + When enabled, cleared ink cannot be restored via undo function + + + Delete all inserted images when clearing canvas + + + Auto compress images larger than 1920x1080 to save memory + + + Show circle center mark when drawing circles + + + Use WPF built-in curve smoothing algorithm + + + Use improved Bezier curve smoothing algorithm for better drawing effect + + + When enabled, ink will not be drawn on canvas but remain in wet ink state, disappearing automatically based on fade time + + + Set time for ink to disappear completely after drawing + + + When enabled, ink fade control switch will not be shown in context menu after clicking pen tool in main toolbar + + + When enabled, will auto save ink at set intervals, only when canvas is visible and has ink + + + When enabled, will use XML format (ISF format) when saving ink, for easy viewing and editing of ink data + + + When enabled, automatically disable two-finger move gesture when exiting whiteboard mode, enable when entering whiteboard mode + + + Allow two-finger or multi-finger zoom operations on selected ink + + + When enabled, automatically switch to eraser when two or more touch points with large area, restore original mode after release + + + Automatically recognize hand-drawn shapes and convert to standard shapes + + + Corrected rectangles do not contain simulated pressure information + + + Corrected triangles do not contain simulated pressure information + + + When drawn line exceeds set length threshold, will automatically adjust to perfect straight line + + + In high-precision mode, take a count point every 10 pixels for more accurate average calculation + + + When drawn line endpoint is close to other line endpoints, will automatically snap and connect + + + Show button to modify name list in random roll call window + + + Show random draw and single draw buttons in random roll call window + + + Enable quick draw floating button + + + Use external roll call tool instead of built-in function + + + Set delay time for single draw window to auto close + + + Set maximum number of people for single random roll call + + + Use new roll call interface design + + + Use machine learning algorithm to avoid drawing same person repeatedly + + + Set history record count for avoiding repetition + + + Set weight for machine learning algorithm to avoid repetition + + + Select background for roll call window + + + Show page turn control buttons in PPT slideshow mode + + + Increase for upward offset, decrease for downward offset, set to 0 for no offset and center placement + + + Increase for rightward offset, decrease for leftward offset, set to 0 for no offset and center placement + + + Adjust position of left bottom button + + + Adjust position of right bottom button + + + Adjust transparency of left page turn button + + + Adjust transparency of right page turn button + + + Adjust transparency of left bottom page turn button + + + Adjust transparency of right bottom page turn button + + + Auto switch to annotation mode when entering PPT slideshow + + + Allow two-finger gestures in slideshow mode + + + When enabled, also show gesture button in PPT slideshow mode + + + Select display position of time capsule on screen + + + When enabled, will record last played page number and auto jump when clicking Yes + + + Auto jump to first page when entering PPT slideshow + + + Show tip when playing hidden slides + + + Show tip when PPT auto plays + + + Need to set password when enabled + + + Set or modify security password + + + Password verification when closing InkCanvasForClass + + + Verification when opening settings panel/new settings window + + + Secondary verification before resetting Settings.json + + + Password verification before modifying or clearing name list + + + When enabled, key files will be occupied as read-only + + + {Binding Description} + + + Auto save current canvas screenshot when clearing canvas + + + Save screenshots to different folders by date + + + Save current canvas ink data when taking screenshots + + + When enabled, will auto screenshot when turning pages if slide has ink + + + Set minimum ink amount required for auto screenshot + + + When enabled, will delete all files with .icstk and .png extensions in auto save directory + + + Select application theme style + + + Show animation effect at startup + + + Select startup animation style + + + Select floating toolbar icon style + + + Adjust floating toolbar size + + + Adjust floating toolbar opacity + + + Floating toolbar opacity in PPT mode (effective after re-entering PPT slideshow) + + + Show nib mode toggle button in color palette window + + + Use legacy floating bar button UI style + + + Show shape button in floating bar + + + Show undo button in floating bar + + + Show redo button in floating bar + + + Show clear button in floating bar + + + Show whiteboard button in floating bar + + + Show hide button in floating bar + + + Show lasso select button in floating bar + + + Show clear and mouse button in floating bar + + + Show quick color palette in floating bar + + + Select quick color palette display mode + + + Show icon in taskbar system tray + + + Scale whiteboard interface to 80% + + + Show current time and date in whiteboard interface + + + Show inspirational quotes in whiteboard interface + + + Show quick operation panel in folded mode + + + Select unfold button icon style + + + When enabled, automatically switch to annotation mode when exiting fold mode for quick annotation + + + When enabled, the floating bar will automatically fold after exiting PPT slideshow + + + When enabled, will automatically dock to sidebar when exiting whiteboard mode + + + Use the old version of the timer button interface + + + Use the new style timer interface + + + Continue counting up after timer timeout + + + Display numbers in red after timer timeout + + + Set the volume of the timer reminder sound + + + Enable progressive reminder function + + + Set the volume of the progressive reminder sound + + + Select the historical version to rollback to + + + Rollback to the version selected in the dropdown + + + Download and install the latest version based on the selected channel, can be used to fix corrupted installations + + + Regularly check if new versions are available + + + Automatically install updates when software is not in use, no manual operation required + + + Use UIA method to achieve topmost, requires administrator privileges + + + Whiteboard booth opens Seewo Video Showcase + + + When enabled, the whiteboard toolbar Booth button launches Seewo Video Showcase (must be installed). When disabled, the built-in booth is used. + + \ No newline at end of file diff --git a/Ink Canvas/Properties/Strings.enUS.xml b/Ink Canvas/Properties/Strings.enUS.xml deleted file mode 100644 index 84f509cb..00000000 --- a/Ink Canvas/Properties/Strings.enUS.xml +++ /dev/null @@ -1,766 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Plugins - - - Startup - - - Canvas - - - Crash Action - - - Gesture - - - Ink Recognition - - - PPT - - - Advanced - - - Automation - - - Random Picker - - - Theme - - - Shortcuts - - - About - - - InkCanvasforClass - - - Booth / Screenshot resolution - - Gesture - Appearance - PPT - Advanced - Automation - Random Picker - Shortcuts - Collapse sidebar - Show sidebar - Control via icc:// protocol - Settings - Changes are saved automatically; some require restart. - Restart - Reset - Exit - Mode - Choose run mode. In PPT-only mode the app is hidden until slide show. (Experimental) - Normal - PPT only - New settings window - Open a new settings window. (In development) - Open new settings - Plugins - Extend Ink Canvas with plugins. Enable, disable, or load custom plugins. - Open plugin manager - Startup - No-focus mode - Borderless - Topmost - UIA topmost - # UIA topmost requires admin to take effect. - Auto-update - Silent update - # Silent update installs when app is idle. - Update channel - Stable (Release) - Preview - Beta - # Stable for reliability; Preview for new features. - Check for updates - # Check and download now. - Version fix - # Download and install latest for current channel. - Rollback - # Open rollback page. - # When silent update is off, you will be prompted after download. - Silent update time range - Start time - End time - # If end < start… - Run at login - Minimize to sidebar at startup - Canvas & ink - Show pen cursor - Pressure-sensitive touch - # Touch devices will support pressure. - Ignore pressure - # Ignore all device pressure. - Eraser size - Very small - Small - Medium - Large - Very large - # Takes effect on next area eraser use. - Hide ink when leaving canvas - # When enabled… - Clear ink history when clearing - Clear images with canvas - Compress images >1920×1080 - Keep hyperbola asymptotes - Yes - No - Ask each time - # Disabling may cause undo bugs. - Show circle center - Whiteboard booth opens Seewo Video Showcase - When enabled, the whiteboard toolbar Booth button launches Seewo Video Showcase (must be installed). When disabled, the built-in booth is used. - WPF default Bezier smoothing - Advanced curve smoothing (recommended) - Ink fade - # Ink will not be drawn on canvas when enabled. - Ink fade time - Hide fade in pen menu - # Fade control will be hidden in pen context menu. - Color - Default - Black - White - Red - Yellow - Blue - Green - Orange - Purple - Update downloaded. It will install when you close the app. - Update ready - Update download failed. Please check your network and try again. - Download failed - Version {0} skipped; you will not be prompted until a newer version is released. - Version skipped - An unexpected error occurred. Save your ink and restart the app. - Too many restarts - App has restarted 5 times. Auto-restart stopped. Contact the developer or check the system. - Starting Ink Canvas... - Crash action - Choose what to do when an unhandled exception occurs: - Silent restart - No action - # Silent restart: automatically restart without prompt. No action: only log, do not restart. - Gestures - Auto-toggle two-finger move in/out of whiteboard - # When enabled: leaving canvas disables two-finger move; entering whiteboard enables it. - Allow rotate & scale selected ink - # Allows scaling selected ink with two or more fingers (independent of rotate setting). - Enable palm eraser - Palm eraser sensitivity - Low sensitivity - Medium sensitivity - High sensitivity - # Low: larger area/more touches required (less false positive); High: easier to trigger but may mis-detect fingers. - Ink correction - Enable ink recognition - Recognition engine - # Auto: WinRT on 64-bit (Windows 10+), IACore on 32-bit. You can force IACore or WinRT. - Auto - IACore - WinRT - WinRT: recognized text to handwriting glyphs - # When the ink correction API runs: WinRT recognizes ink words, then replaces recognized text with outline strokes from a handwriting-style font (default Ink Free / KaiTi / Segoe Script; set handwritingCorrectionFontFamily in Settings JSON). 64-bit + WinRT. - Block fake pressure on corrected rectangles - Block fake pressure on corrected triangles - Correct freehand triangles - Correct freehand rectangles - Correct circles and ellipses - Auto-straighten lines - Length threshold - Sensitivity - High-precision straightening - # When enabled, lines longer than the threshold will be straightened. Sensitivity 0.05–2.0: smaller = stricter; larger = easier to treat as straight. High-precision samples every 10px for better judgement. - Line endpoint snapping - Snapping distance - Personalization - Theme - Light theme - Dark theme - Follow system - Enable startup animation - Startup animation style - Random - Follow seasons - Spring - Summer - Autumn - Winter - Year-of-Horse special - Floating toolbar icon - “ICC-CE” default - “ICC-CE” no shadow - “ICC-CE” dark - “ICC-CE” dark breathing - “ICC-CE” white transparent - “ICC-CE” black transparent - Coolapk cross-eye emoji - Coolapk abused emoji - Coolapk grin emoji - Coolapk underwear emoji - Coolapk green-hat Doge - Tieba emoji - Custom floating icon - Upload - Manage - Floating toolbar scale - Floating toolbar opacity - Floating bar opacity in PPT - # Takes effect after re-entering slide show - Show nib-mode button in palette - Whiteboard UI 80% scale - Show time and date in whiteboard - Show quotes in whiteboard - Where is the quote from? - osu! player quotes - Inspirational mottos - Gaokao blessings - Hitokoto API - Custom - Enable quick panel in docked mode - Un-dock button icon - Arrow - Pen - Floating bar buttons - Use legacy floating bar UI - Show shape button - Show undo button - Show redo button - Show clear button - Show whiteboard button - Show hide button - Show lasso select button - Show clear+mouse button - Show quick palette - Quick palette display mode - Single row (6 colors) - Double row (8 colors) - Eraser button display - Show both - Area eraser only - Line eraser only - Hide all - Taskbar tray icon - Enable tray icon - PPT integration - These settings apply during slide show and override others. - Microsoft PowerPoint support - PowerPoint enhancement - Steal focus to skip animations (PPT) - Use ROT integration - WPS support - Kill WPP process (avoid leftovers) - # When disabled, leftover WPP processes may cause slow close or cannot exit completely. - # If you only use PowerPoint, do not enable WPS integration. If you use WPS, it is recommended not to use PowerPoint together. - Enabling WPS support may cause lag when closing WPS! - # WPS is supported, but MS Office and WPS cannot be supported at the same time. To enable WPS support, make sure “WPS Office compatibility with third-party systems and software” is enabled in the WPS config tool, otherwise WPS cannot be detected. - Hide ink when exiting board mode - # When this option is on, ink will not be shown in PPT mode if not in annotation mode. - Clear ink history when clearing ink - Clear images when clearing canvas - Auto-compress images when inserting (larger than 1920x1080) - PPT page-turn buttons - Show page-turn buttons in PPT mode - Bottom left - Bottom right - Left - Right - Left offset - Left opacity - Right offset - Right opacity - # Increase for up, decrease for down; 0 = no offset, centered. - Bottom left offset - Bottom left opacity - Bottom right offset - Bottom right opacity - # Increase for right, decrease for left; 0 = no offset, centered. - Sides - Show page number - Half opacity - Black background - Bottom left & right - PPT page button clickable - # When enabled, clicking the page button opens PowerPoint grid thumbnails. Not supported in WPS. - PPT long-press to turn page - # When enabled, long-press on PPT page button to turn pages continuously. - # With UIA topmost on, app needs admin to stay on top. To turn off, fully quit then start again; restart will not disable it. - Silent update - # Silent update installs when the app is idle; no manual action needed. - Update channel - Stable (Release) - Preview - Beta - # Stable: reliable updates. Preview: new features with better stability than Beta. Beta: earliest new features. - Check for updates - # Check and download the latest version now. - Repair installation - # Repair downloads the latest build for the selected channel and reinstalls; use to fix broken installs. - Rollback to previous version - # Opens a page to manually roll back to an earlier version. - # When silent update is off, you will be prompted after download. When on, every 10 minutes the app checks: 1) within silent-update time window 2) not in writing mode 3) not in canvas. If all pass, it will close and update. - Silent update time window - Start time - End time - # If end < start, end is next day. If start = end, window is 24h. - Run at startup - Dock to sidebar after startup - Canvas and ink - Show pen cursor - Enable pressure-sensitive touch - # When on, touch screens that support pressure will show pressure; for devices not recognized by the system. - Ignore pressure - # When on, all strokes use uniform thickness; mutually exclusive with pressure-sensitive touch. - Eraser size - Very small - Small - Medium - Large - Very large - # Change takes effect next time you use area eraser. - Keep hyperbola asymptotes - Yes - No - Ask each time - # If not kept, undo-related bugs may occur. - Show circle center when drawing - Use WPF default Bezier smoothing - Use advanced curve smoothing (recommended) - Enable ink fade - # When on, ink is not committed to canvas; it fades after the set time. - Ink fade time - Hide ink fade control in pen menu - # When on, the pen context menu will not show the ink fade control. - Enable brush auto-restore - # When on, temporary brush changes will restore at the configured time(s) to the color/opacity/width set here. - Auto-restore time points (HH:mm, multiple with ;) - Restore color - Default - Black - White - Red - Yellow - Blue - Green - Orange - Purple - Restore stroke width - Restore opacity - Switch back to annotation after eraser - # When on, after erasing, staying idle for a while will switch back to annotation mode. - Auto switch delay - # If you erase again within the delay, the timer resets. - # When on, line endpoints near other endpoints will snap and connect. - Enter annotation mode when starting PPT slide show - Conflicts with "Auto fold when playing PPT" in Automation! - Allow two-finger gestures in slide show - Allow finger gesture to turn slides - # When canvas is on, finger swipe (not pen) can turn slides in show mode when canvas has no ink. - Show gesture buttons in PPT slide show - # When on, gesture buttons are shown in PPT slide show. - PPT time capsule - # When on, show time capsule in PPT show; can replace minimized timer window. - Time capsule position: - Top left - Top right - Top center - Show quick panel in PPT slide show - # When off, quick panel is hidden in PPT slide show. - Auto screenshot on slide change - # When on, auto-screenshot when turning page with ink on slide. - Auto-save slide ink - # When on, ink is saved when ending slide show and loaded next time (same file and page). - Remember and prompt last slide position - # When on, last page is recorded; choose Yes to jump to it. - Go to first slide when entering show - Warn about hidden slides - Warn if auto-play is enabled - Advanced - Adjust when finger-touch shows circle eraser or palm eraser is much larger than palm - Special screen mode - Touch multiplier - Tap with pen in the area below to estimate touch size multiplier - # Value is for reference only - Bind eraser to touch size multiplier - # BoundsWidth is used as contact area threshold - Quad IR mode - Enable logging - Save logs by date - # Log files over 512 KB are auto-deleted. With date save, logs go to Logs folder; folder is cleared when over 5 MB. - Confirm exit with dialog - Enable FullScreenHelper - Experimental - # Thanks to lindexi for FullScreenHelper; reduces taskbar pop-up and supports multi-monitor fullscreen. Disable if you see odd issues; restart ICC to apply. - Enable AvoidFullScreenHelper - # Avoid canvas fullscreen; may fix taskbar not on top and Win11 taskbar unclickable. Can cause floating bar offset with AppBar on left/top. Restart ICC to apply. - Enable EdgeGestureUtil - More features - Timer - Random draw - Single draw - Save - Open... - Replay - Screenshot - Manual - Settings - Single draw - Random draw - Timer - Whiteboard - Exit slide show - Show - Exit - Settings backup & restore - # You can manually back up current settings or restore previous backups; backups are also created automatically before updates. - Backup before update - Periodic auto-backup - Backup interval - 1 day - 3 days - 7 days - 14 days - 30 days - (default: 7 days) - Backup now - Restore backup - Config profiles & hot reload - # Selecting a profile switches and hot-reloads it; \"Save as\" saves current settings as a new profile. - Profile: - Delete profile - Save as profile - Automation - Auto fold - Seewo Whiteboard 5 - Seewo Visual Presenter - Seewo Whiteboard 3 - Seewo Lite Whiteboard - Seewo Lite Whiteboard 5C - Seewo Pinco - HiteBoard - Hite visual presenter - Hite Lite Whiteboard - WenXiang Whiteboard - Microsoft Whiteboard - Admox Whiteboard - Admox visual presenter - YiYun Whiteboard - YiYun visual presenter - MaxHub Whiteboard - Ignore EN5 desktop annotation window when auto folding - Auto fold when entering old ZhongYuan whiteboard - Auto fold while playing PPT - Keep folded after app exit - # When on, apps that trigger auto fold will stay folded even after they exit. - Auto kill - Auto kill Seewo PPT tools - # Killing PPT tools disables Seewo classroom helper. Delete Office.dll in its install folder to stop the PPT toolbar without auto kill. - Auto kill Seewo Whiteboard 5 - Auto kill Hite screen writing - Enter annotation after killing Hite screen writing - Auto kill YouJiao teacher - Auto kill Seewo Desktop 2.0 annotation - # Seewo Desktop 2.0 annotation is 64-bit so ICC (32-bit) cannot inspect it deeply; only process name DesktopAnnotation is matched. If you have another app with the same name, keep this off. - Kill similar apps - Auto kill Ink Canvas and IC+ - Auto kill ICA (both new & old) - Auto kill Inkeys (new only) - File association - Manage .icstk file association so double-click opens in Ink Canvas. - Remove association - Check status - Register association - Floating window interceptor - Detect and block floating windows from similar software - Enable floating window interceptor - Interceptor not running - Auto screenshot on clear - Save screenshots in date folders - Auto-save ink when screenshotting - Auto-save ink periodically - Save interval - 1 minute - 3 minutes - 5 minutes - 10 minutes - 15 minutes - 30 minutes - 60 minutes - # When on, strokes are auto-saved at the set interval, only when canvas is visible and has ink. - Save full-page strokes - # When on, auto/manual saves store all pages in fullscreen; multiple pages are packed in one archive (whiteboard strokes open only in whiteboard mode; PPT strokes only in slide show mode). - Save as XML format - # When on, strokes are saved as XML (ISF) for easier inspection and editing. - Minimum ink for auto screenshot - Stroke and screenshot save path - Browse - Set save path to D:\Ink Canvas - Set save path to Documents - # Please ensure the save folder is writable. - Auto delete old strokes and screenshots - # When on, all .icstk and .png files in the auto-save folder may be deleted! - Retention duration - days - Cloud storage management - Fold mode - Switch to annotation when exiting fold mode - # When on, exiting fold mode switches back to annotation for convenience. - Auto fold floating bar after PPT show - # When on, floating bar is auto-folded after exiting PPT slide show. - Auto fold when exiting whiteboard - # When on, exiting whiteboard folds back to sidebar. - Random roll call - Show button to edit name list - Roll-call window background (legacy UI only) - Background: - Default background - Custom background: - Upload - Manage - Enable random & single-draw buttons - Enable quick-draw floating button - Use external roll-call app - Roll-call type - ClassIsland - SecRandom - NamePicker - Single-draw window close delay - Max students per single draw - New roll-call UI - Enable new roll-call UI - Use machine learning to avoid repeats - History count for avoidance - Avoidance weight - # ML analyzes recent roll-call history to avoid repeating the same students. - Timer settings - Use legacy timer button UI - New timer UI - Enable count-up after timeout - Highlight numbers when overtime - Timer alert volume - Custom alert sound: - Select file - Reset - Progressive reminder - Progressive reminder volume - Custom progressive reminder audio: - Select file - Reset - About - Device information - Device ID: - Usage frequency: - Update priority: - Launch count: - Total usage time: - Loading... - Refresh device info - I have read and agree to the - privacy statement - Anonymous usage data upload: - Off (no upload) - Upload basic data - Upload basic + optional data - # Before using or distributing this software, you must be aware of the related open-source licenses. This software is based on https://github.com/WXRIW/Ink-Canvas. - This software, ICA and Ink Canvas are all open sourced under a license - The strong copyleft license requires that complete source code and modifications of the licensed work (including large works using it) be provided under the same license. Copyright and license notices must be retained. Contributors explicitly grant patent rights. - Developers: - Developer of ICC CE - Developer of ICC - Developer of ICA - Developer of Ink Canvas - ICC repository: - ICA repository: - Ink Canvas repository: - Thanks to the following contributors: - © 2025-2026 CJK_mkp. All rights reserved. - We love open-source forever! - Version: - Close - On - Off - External URI scheme (icc://) - Nib mode BoundsWidth - Finger mode BoundsWidth - # EdgeGestureUtil is newly introduced in ICC to temporarily block edge gestures when using touch (e.g., on Windows 10: swipe from the left edge to Task View, from the right edge to Action Center; on Windows 11: swipe up from the bottom to open Start). It works by using - (When the app window is active and in full-screen mode (or an owned window is active), prevents edge gesture behavior.) If anything is abnormal, turn this option off; it should take effect immediately. (Not available on Windows 7/8.) - Enable ForceFullScreen - # When a window size change is detected, automatically uses Win32 API to set this window size to the primary monitor size (in device pixels). Turn it off if you don't need it; takes effect immediately. - Enable DPIChangeDetection - # When a system DPI change is detected, it tries to keep FloatingBar visible. If it goes off-screen, it will attempt to move it into the visible area. (Increasing DPI may trigger this; decreasing DPI won't auto-move—adjust manually.) - Enable ResolutionChangeDetection - # When a screen resolution change is detected, it tries to keep FloatingBar visible. If it goes off-screen, it will attempt to move it into the visible area. (Reducing resolution may trigger this; if it's still on-screen it won't auto-adjust—adjust manually.) - Seewo Whiteboard 3 - Seewo Whiteboard 5 - Seewo Whiteboard 5C - Seewo Pinco - Seewo Pinco pen - Seewo PPT Tools - AiClass - Hite screen writing - Changyan smart classroom - Changyan PPT - Tianyu Education Cloud - Seewo desktop pen - Seewo desktop sidebar - Multi-touch writing - Two-finger move - Two-finger zoom - Two-finger rotate - Background - Select - Pen - Highlighter - Eraser - Eraser options - Size - Eraser shape - Circle - Blackboard - Clear ink - Clear ink & history - Stroke eraser - Shapes - (Long-press in first row to keep selected) - Auto-hide - Insert image - Select image - Screenshot - Undo - Redo - Tools - Exit - New page - Previous - Next - Page - Delete this page - Test text - Exit - Thickness - Dark - Background - Hide canvas - Check - From start slideshow - End slideshow - One finger drag - Restore - Clear & Hide - Mouse - Annotate - Clear - Visual presenter - Captured photos - Camera devices - Present - Correct - Capture - Rotate - UI language - Follow system - Chinese (Simplified) - English - You need to restart the app for language changes to fully take effect. - Area eraser - Stroke eraser - Lasso - Geometry - Clear & cursor - Board - Hide - Geometry drawing - Line - Dashed line - Dotted line - Arrow - 4 parallel lines - Centered square - Centered circle - Centered dashed circle - Centered ellipse - Cuboid - Square - Cylinder - Cone - Gesture - Gesture options - Multi-touch writing - Two-finger move - Two-finger zoom - Two-finger rotate - Gesture - Gesture options - diff --git a/Ink Canvas/Properties/Strings.resx b/Ink Canvas/Properties/Strings.resx index df7fb722..483d13e0 100644 --- a/Ink Canvas/Properties/Strings.resx +++ b/Ink Canvas/Properties/Strings.resx @@ -1,5 +1,64 @@ + @@ -118,668 +177,2453 @@ 展台/截图分辨率 展台分辨率选项卡提示 - 手势设置 - 个性化设置 - PPT设置 - 高级设置 - 自动化设置 - 随机窗口设置 - 快捷键设置 - 折叠侧边栏 - 显示侧边栏 - 通过 icc:// 协议从外部控制软件 - 设置 - 设置更改将自动保存,部分设置需要重启软件后生效 - 重启 - 重置 - 退出 - 模式设置 - 选择软件运行模式。仅PPT模式下,软件将完全隐藏,仅在PPT放映时出现。(实验性功能,可能不稳定。) - 正常模式 - 仅PPT模式 - 新设置窗口 - 打开新的设置窗口,提供更丰富的设置选项和更好的用户体验。(开发中) - 打开新设置窗口 - 插件管理 - 通过插件扩展InkCanvas的功能。您可以启用或禁用插件,或加载自定义插件。 - 打开插件管理器 - 启动 - 窗口无焦点模式 - 窗口无边框模式 - 窗口置顶 - UIA置顶 - # 开启UIA置顶后,软件需要管理员启动才能置顶… - 自动检查更新 - 静默更新 - # 静默更新将在软件不使用时自动安装,无需手动操作 - 更新通道 - 稳定版 (Release) - 预览版 (Preview) - 测试版 (Beta) - # 稳定版提供可靠更新,预览版提供新功能体验… - 手动更新 - # 点击后立即检查并下载最新版本 - 版本修复 - # 版本修复会根据当前选择的通道下载最新版本并执行安装… - 历史版本回滚 - # 历史版本回滚,点击后会弹出相应页面… - # 关闭静默更新后,已完成安装包的下载后将会弹窗询问… - 静默更新时间段 - 起始时间 - 终止时间 - # 若终止时间小于起始时间… - 开机时运行 - 开机运行后收纳到侧边栏 - 画板和墨迹 - 显示画笔光标 - 启用压感触屏模式 - # 开启后,触屏设备也将支持压感效果… - 屏蔽压感 - # 开启后,将忽略所有设备的压感信息… - 橡皮大小 - 很小 - 较小 - 中等 - 较大 - 很大 - # 非实时切换,下一次使用面积擦时生效。 - 退出画板模式后隐藏墨迹 - # 开启 退出画板模式后隐藏墨迹 选项后… - 清空墨迹时删除墨迹历史记录 - 清空画布时同时清空图片 - 插入图片时自动压缩(大于1920x1080) - 保留双曲线渐近线 - - - 每次询问 - # 请注意,若不保留双曲线渐近线可能会有遇到撤回相关的 BUG… - 绘制圆时显示圆心位置 - 白板展台按钮启动希沃视频展台 - 开启后,点击白板工具栏「展台」将打开希沃视频展台(需已安装);关闭则使用内置展台。 - 使用WPF默认贝塞尔曲线平滑 - 使用高级曲线平滑(推荐) - 启用墨迹渐隐功能 - # 开启后墨迹不会绘制到画布上… - 墨迹渐隐时间 - 在笔工具菜单中隐藏墨迹渐隐控制 - # 开启后,主工具栏上点击笔工具后弹出的上下文菜单中将不显示… - 颜色 - 默认 - 黑色 - 白色 - 红色 - 黄色 - 蓝色 - 绿色 - 橙色 - 紫色 - 更新已下载完成,将在软件关闭时自动安装。 - 更新已准备就绪 - 更新下载失败,请检查网络连接后重试。 - 下载失败 - 已设置跳过版本 {0},在下次发布新版本之前不会再提示更新。 - 已跳过此版本 - 抱歉,出现未预期的异常,可能导致 InkCanvasForClass 运行不稳定。\n建议保存墨迹后重启应用。 - 重启次数过多 - 检测到程序已连续重启5次,已停止自动重启。请联系开发者或检查系统环境。 - 正在启动 Ink Canvas... - 崩溃后操作 - 请选择软件发生未处理异常时的自动操作: - 静默重启软件 - 无操作 - # 静默重启:崩溃后自动重启软件,无提示。无操作:崩溃后仅记录日志,不自动重启。 - 手势 - 进退白板模式自动开关双指移动功能 - # 开启后退出画板模式时自动关闭双指移动手势,进入白板模式时自动开启双指移动手势 - 允许双指旋转与缩放选中的墨迹 - # 允许选中墨迹后对墨迹进行双指或多指缩放操作(此设置不受“允许双指旋转”设置的影响) - 启用手掌擦 - 手掌擦敏感度 - 低敏感度 - 中敏感度 - 高敏感度 - # 低敏感度:需要更大的触摸面积和更多触摸点,减少误判;高敏感度:更容易触发手掌擦,但可能误判手指。 - 墨迹纠正 - 启用墨迹识别 - 识别引擎 - 自动:64 位进程使用 WinRT(Windows 10+),32 位使用 IACore。可强制指定 IACore 或 WinRT。 - 自动 - IACore - WinRT - WinRT 识别转手写体字形 - # 开启后,调用墨迹纠正 API 时:先 WinRT 识别手写词,再将识别成功的文字用手写风格字体(默认 Ink Free / 楷体 等,可在设置 JSON 的 handwritingCorrectionFontFamily 调整)转成字形轮廓墨迹替换原笔画。需 64 位与 WinRT。 - 阻止矫正后的矩形带有模拟压感值 - 阻止矫正后的三角形带有模拟压感值 - 矫正手绘三角形 - 矫正手绘矩形 - 矫正手绘圆形与椭圆 - 直线自动拉直 - 长度阈值 - 灵敏度 - 高精度直线拉直 - # 开启后,当绘制的直线超过设定长度阈值时,将自动调整为完美直线。灵敏度范围0.05-2.0,越小要求越严格,越弯曲的线条越不容易被拉直;值越大越容易识别为直线。高精度模式下,每隔10像素取一个计数点,获取更准确的平均值用于判断。 - 直线端点吸附 - 吸附距离 - 个性化 - 主题 - 浅色主题 - 深色主题 - 跟随系统 - 启用启动动画 - 启动动画样式 - 随机 - 跟随四季 - 春季 - 夏季 - 秋季 - 冬季 - 马年限定 - 浮动工具栏图标 - “ICC-CE”默认 - “ICC-CE”无阴影 - “ICC-CE”深色 - “ICC-CE”深色呼吸版 - “ICC-CE”白色透明版 - “ICC-CE”黑色透明版 - 酷安斗鸡眼滑稽 - 酷安受虐滑稽 - 酷安呲牙笑 - 酷安头戴内裤滑稽 - 酷安绿帽Doge - 贴吧滑稽 - 自定义浮动栏图标 - 上传 - 管理 - 浮动工具栏缩放 - 浮动工具栏透明度 - 浮栏在PPT下透明度 - # 重新进入PPT放映后生效 - 在调色盘窗口中显示 笔尖模式 按钮 - 白板 UI 80% 缩放 - 在白板中显示当前时间和日期 - 在白板中显示信仰の源1(好喝的/毒的鸡汤) - 信仰の源出自Where? - osu!玩家语录 - 励志立志的名言警句 - 高考祝福语 - 一言(Hitokoto API) - 自定义 - 在收纳模式下启用快速面板 - 取消收纳按钮图标 - 箭头 - - 浮动栏按钮显示 - 使用老版浮动栏按钮UI - 显示形状按钮 - 显示撤销按钮 - 显示重做按钮 - 显示清空按钮 - 显示白板按钮 - 显示隐藏按钮 - 显示套索选择按钮 - 显示清并鼠按钮 - 显示快捷调色盘 - 快捷调色盘显示模式 - 单行显示(6色) - 双行显示(8色) - 橡皮按钮显示 - 两个都显示 - 仅显示面积擦 - 仅显示线擦 - 都不显示 - 任务栏托盘图标 - 启用托盘图标 - PPT联动 - 此部分的设置项将会在幻灯片播放时使用,优先级高于其他设置项 - Microsoft PowerPoint 支持 - PowerPoint 联动增强 - 抢占焦点放映(用于跳过PPT动画) - 使用 ROT 联动 - WPS 支持 - WPP进程查杀(防止WPP残留进程) - # 关闭后将不会自动查杀WPP残留进程,可能导致WPP关闭卡顿或无法彻底退出。 - # 如果您只使用PowerPoint请不要打开WPS联动开关,如果使用WPS建议不要使用PowerPoint! - 开启WPS支持后会导致WPS关闭时卡顿! - # 可支持 WPS,但目前无法同时支持 MSOffice 和 WPS。若要启用WPS支持,请确保 WPS 是否在 “配置工具” 中开启了 “WPS Office 兼容第三方系统和软件” 选项,否则将无法识别到WPS! - 退出画板模式后隐藏墨迹 - # 开启 退出画板模式后隐藏墨迹 选项后,进入 PPT 模式时未处于批注模式时不会显示墨迹。 - 清空墨迹时删除墨迹历史记录 - 清空画布时同时清空图片 - 插入图片时自动压缩(大于1920x1080) - PPT翻页按钮 - 在 PPT 模式下显示翻页按钮 - 左下 - 右下 - 左侧 - 右侧 - 左侧偏移 - 左侧透明度 - 右侧偏移 - 右侧透明度 - # 调大往上偏移,调小往下偏移,修改为0为不偏移,居中放置 - 左下偏移 - 左下透明度 - 右下偏移 - 右下透明度 - # 调大往右偏移,调小往左偏移,修改为0为不偏移,居中放置 - 两侧 - 显示页码 - 半透明 - 黑色背景 - 左下右下 - PPT 页码按钮可点击 - # 开启该选项后,点击页码按钮可以唤起PowerPoint自带的网格缩略图视图。WPS不支持该功能,开启也没用。 - PPT 翻页按钮长按翻页 - # 开启该选项后,长按PPT翻页按钮可以连续翻页,提高翻页效率。 - # 开启UIA置顶后,软件需要管理员启动才能置顶,关闭此功能需要完全关闭软件后再手动启动,无法使用重启来关闭此功能 - 静默更新 - # 静默更新将在软件不使用时自动安装,无需手动操作 - 更新通道 - 稳定版 (Release) - 预览版 (Preview) - 测试版 (Beta) - 软件架构 - 32 位 (x86) - 64 位 (x64) - # 选择要下载架构 - # 稳定版提供可靠更新,预览版提供新功能体验同时拥有相较Beta版更强的稳定性,测试版提供新功能抢先体验 - 手动更新 - # 点击后立即检查并下载最新版本 - 版本修复 - # 版本修复会根据当前选择的通道下载最新版本并执行安装,可用于修复损坏的安装 - 历史版本回滚 - # 历史版本回滚,点击后会弹出相应页面供用户手动回滚到之前的版本 - # 关闭静默更新后,已完成安装包的下载后将会弹窗询问是否进行更新,开启静默更新后将会在安装包下载完成后每隔十分钟进行如下检测:①处于静默更新时间段内 ②未处于书写模式 ③未处于画板内。若以上检测通过即会关闭软件进行自动更新。 - 静默更新时间段 - 起始时间 - 终止时间 - # 若终止时间小于起始时间,即将终止时间视为第二天的时间。# 若起始时间与终止时间相同,即视为全天候时间。 - 开机时运行 - 开机运行后收纳到侧边栏 - 画板和墨迹 - 显示画笔光标 - 启用压感触屏模式 - # 开启后,触屏设备也将支持压感效果,适用于部分支持压感但无法被系统识别的触屏设备。 - 屏蔽压感 - # 开启后,将忽略所有设备的压感信息,使所有笔画具有统一的粗细。与压感触屏模式互斥。 - 橡皮大小 - 很小 - 较小 - 中等 - 较大 - 很大 - # 非实时切换,下一次使用面积擦时生效。 - 保留双曲线渐近线 - - - 每次询问 - # 请注意,若不保留双曲线渐近线可能会有遇到撤回相关的 BUG 影响用。 - 绘制圆时显示圆心位置 - 使用WPF默认贝塞尔曲线平滑 - 使用高级曲线平滑(推荐) - 启用墨迹渐隐功能 - # 开启后墨迹不会绘制到画布上,而是保持湿墨迹状态,根据设置的渐隐时间自动消失 - 墨迹渐隐时间 - 在笔工具菜单中隐藏墨迹渐隐控制 - # 开启后,主工具栏上点击笔工具后弹出的上下文菜单中将不显示墨迹渐隐控制开关 - 启用画笔自动恢复 - # 启用后,临时修改画笔设置后将在指定时间点自动恢复到你在此处配置的颜色 / 透明度 / 粗细 - 自动恢复时间点 (HH:mm,可多个,用 ; 分隔) - 恢复目标颜色 - 默认 - 黑色 - 白色 - 红色 - 黄色 - 蓝色 - 绿色 - 橙色 - 紫色 - 恢复笔粗细 - 恢复透明度 - 使用橡皮擦后自动切换回批注模式 - # 开启后,使用橡皮擦进行擦除操作后静置一段时间将自动切换回批注模式 - 自动切换延迟时间 - # 若在计时时间内再次进行擦除操作,计时器将重新开始计时 - # 开启后,当绘制的直线端点靠近其他直线端点时,将自动吸附连接。 - 进入 PPT 放映时自动进入批注模式 - 该项与"自动化"中的"播放PPT时自动收纳"选项冲突! - 允许幻灯片模式下的双指手势 - 允许使用手指手势进行幻灯片翻页 - # 允许开启画板时使用手指手势进行幻灯片翻页(启用后,在幻灯片放映模式下,当画板无墨迹时,使用手指(笔尖或手掌无法识别)左右滑动即可控制幻灯片翻页。) - PPT 放映模式显示手势按钮 - # 开启后在 PPT 放映模式下也显示手势按钮 - PPT时间显示胶囊 - # 开启后在 PPT 放映模式下显示时间胶囊,可替代最小化计时器窗口 - 时间胶囊位置: - 左上角 - 右上角 - 顶部居中 - PPT 放映时显示快速面板 - # 关闭后在 PPT 放映时不显示快速面板 - 自动幻灯片截屏 - # 开启 自动幻灯片截屏 后将会在幻灯片有墨迹时翻页自动截屏 - 自动保存幻灯片墨迹 - # 开启 自动保存幻灯片墨迹 后将在结束幻灯片放映时自动将保存已有墨迹,并在下次打开时自动加载(文件名和幻灯片页数都要相同) - 记忆并提示上次播放位置 - #开启后会记录上次播放的页数,点击"是"后会自动跳转 - 进入放映时回到首页 - 提示隐藏幻灯片 - 提示是否已启用自动播放 - 高级设置 - 可在手指触摸画板时显示圆形橡皮或手掌触摸画板时显示的橡皮比 手掌大很多时调整 - 特殊屏幕模式 - 触摸倍数 - 在下方区域内用笔尖点击以估计触摸大小倍数 - # 数值仅供参考 - 橡皮擦绑定触摸大小倍数 - # BoundsWidth 参数作为接触面积区分界限 - 四边红外模式 - 记录日志 - 日志以日期保存 - # 日志文件超过 512 KB 时会自动删除。开启日期保存后,日志将保存在Logs文件夹中,当文件夹大小超过5MB时自动清空。 - 关闭软件时二次弹窗确认 - 启用FullScreenHelper - 实验性选项 - # 感谢lindexi大佬提供的FullScreenHelper,可以减少任务栏弹出的问题,且支持多显示器自动全屏(虽然对icc来说没什么用就是了),如果遇到一些玄学问题,可以关闭该功能,重启icc后生效。 - 启用AvoidFullScreenHelper - # 避免画布全屏,应该可解决任务栏非置顶和Win11任务栏无法点击的问题,会导致左侧或顶部有AppBar(Dock栏软件)时导致浮动工具栏偏移,重启icc后生效。 - 启用EdgeGestureUtil - 更多功能 - 计时器 - 随机抽 - 单次抽选 - 保存 - 打开... - 重播 - 截屏 - 说明书 - 设置 - 单次抽 - 随机抽 - 计时器 - 白板 - 退出放映 - 显示 - 退出 - 设置备份与还原 - # 可手动备份当前设置或还原之前的备份,自动更新前也会自动备份 - 自动更新前备份 - 定期自动备份 - 备份间隔 - 1天 - 3天 - 7天 - 14天 - 30天 - (默认7天) - 手动备份 - 还原备份 - 配置文件切换与热重载 - # 选择配置文件即切换并热重载;另存为可将当前配置保存为新配置文件 - 配置文件: - 删除配置文件 - 另存为配置文件 - 自动化 - 自动收纳 - 希沃白板5 - 希沃展台 - 希沃白板3 - 希沃轻白板 - 希沃轻白板5C - 希沃品课 - 鸿合白板 - 鸿合展台 - 鸿合轻量白板 - 文香白板 - 微软白板 - 安道白板 - 安道展台 - 艺云白板 - 艺云展台 - MaxHub白板 - 自动收纳忽略桌面EN5批注窗口 - 进入“中原旧白板”时自动收纳 - 播放PPT时自动收纳 - 软件退出后保持收纳模式 - # 开启后,执行自动收纳的软件在软件退出后不退出收纳模式,保持收纳状态 - 自动查杀 - 自动查杀希沃“PPT 小工具” - # 请注意,查杀 PPT 小工具会导致希沃课堂授课助手无法使用,直接进入希沃课堂授课助手安装目录删除 Office.dll 文件即可进入 PPT 放映时不会启动希沃的 PPT 工具栏。 - 自动查杀 希沃白板5 - 自动查杀 鸿合屏幕书写 - 鸿合屏幕书写查杀后自动进入批注 - 自动查杀 优教授课端 - 自动查杀 希沃桌面2.0 桌面批注 - # 由于希沃桌面2.0提供的桌面批注是64位应用程序,icc是32位程序无法访问,所以目前暂不做精准匹配,只匹配进程名称DesktopAnnotation,后面会考虑封装一套基于P/Invoke和WMI的综合进程识别方案。如果遇到同进程名的软件直接不开启该选项就行了,见谅! - 同类软件查杀 - 自动查杀 Ink Canvas 和 IC+ - 自动查杀ICA(新版旧版通杀) - 自动查杀 智绘教Inkeys(仅限新版) - 文件关联管理 - 管理.icstk文件的关联设置,双击.icstk文件可直接在Ink Canvas中打开 - 取消文件关联 - 检查关联状态 - 重新注册关联 - 悬浮窗拦截 - 自动检测并拦截同类软件的悬浮窗 - 启用悬浮窗拦截 - 拦截器未启动 - 清屏时自动截图 - 截图分日期文件夹保存 - 截图时自动保存墨迹 - 定时自动保存墨迹 - 保存间隔 - 1分钟 - 3分钟 - 5分钟 - 10分钟 - 15分钟 - 30分钟 - 60分钟 - # 开启后将在设定时间间隔自动保存墨迹,仅在画布可见且有墨迹时才会保存 - 墨迹全页面保存 - # 开启后自动保存和手动保存墨迹时将以全屏模式保存。如果存在多个画布和墨迹,将把所有页面的墨迹按照每页为单位保存进一个压缩包中(注意,白板的墨迹只能在白板模式下打开,PPT的墨迹只能在PPT放映模式下打开) - 保存为XML格式 - # 开启后保存墨迹时将使用XML格式(ISF格式),便于查看和编辑墨迹数据 - 自动截图最小墨迹量 - 墨迹与截图的保存路径 - 浏览 - 设置保存到 D:\Ink Canvas - 设置保存到 文档 - # 请注意检查保存文件夹是否有写入权限 - 定期自动删除超过保存时间的墨迹、截图文件 - # 请注意如果开启自动删除功能,将会删除自动保存目录下所有后缀名为 .icstk 和 .png 的文件! - 保存时长 - - 云存储管理 - 收纳模式 - 退出收纳模式时自动切换至批注模式 - # 开启后,退出收纳模式时将自动切换至批注模式,便于快速批注 - 退出PPT放映后自动收纳浮动栏 - # 开启后,退出PPT放映后会自动收纳浮动栏 - 退出白板时自动收纳 - # 开启后,退出白板模式时会自动收纳到侧边栏 - 随机点名 - 显示修改随机点名名单的按钮 - 点名窗口背景设置(仅老版点名UI有效) - 背景选择: - 默认背景 - 自定义背景: - 上传 - 管理 - 启用随机抽和单次抽按钮 - 启用快抽悬浮按钮 - 直接调用外部点名 - 点名类型 - ClassIsland点名 - SecRandom点名 - NamePicker点名 - 单次抽人窗口关闭延迟 - 单次随机点名人数上限 - 新点名UI设置 - 启用新点名UI - 启用机器学习避免重复 - 避免重复历史记录数量 - 避免重复权重 - # 机器学习算法会分析最近的点名历史,智能避免重复选择相同人员 - 计时器设置 - 使用老版计时器按钮UI - 新计时器UI - 启用正计时 - 超时醒目数字 - 计时器提醒音量 - 自定义提醒铃声: - 选择文件 - 重置 - 渐进提醒 - 渐进提醒音量 - 自定义渐进提醒音频: - 选择文件 - 重置 - 关于 - 设备信息 - 设备ID: - 使用频率: - 更新优先级: - 启动次数: - 总使用时长: - 正在获取... - 刷新设备信息 - 我已阅读并同意 - 中的隐私说明 - 匿名使用数据上传: - 关闭(不上传) - 上传基础数据 - 上传基础 + 可选数据 - # 使用和分发本软件前,请您应当且务必知晓相关开源协议,且您应当知晓本软件基于 https://github.com/WXRIW/Ink-Canvas 修改而成。 - 本软件和ICA,Ink Canvas均基于许可证开源 - 本强许可协议的许可条件是,在相同许可协议下,提供许可作品的完整源代码和修改,包括使用许可作品的大型作品。版权和许可声明必须保留。贡献者明确授予专利权。 - 开发者: - ICC CE 的开发者 - ICC 的开发者 - ICA 的开发者 - Ink Canvas 的开发者 - ICC 仓库源: - ICA 仓库源: - Ink Canvas 仓库源: - 感谢下列贡献者: - © 2025-2026 CJK_mkp 版权所有 - 我们永远热爱开源! - 版本: - 关闭 - - - 外部协议调用 (icc://) - 笔尖模式 BoundsWidth - 手指模式 BoundsWidth - # EdgeGestureUtil是icc最新引入的可以暂时阻止在使用触摸时触发边缘手势(如Windows10环境下,屏幕左边缘滑动进入任务视图,右边缘滑动弹出通知中心;Windows11环境下,底部向上滑动打开开始菜单),其原理是使用了 - (当应用程序窗口处于活动状态且处于全屏模式 (或拥有的窗口) 处于活动状态时,防止边缘手势行为。)来实现的。如果有异常,请关闭该选项,该选项应该能够实时生效。(Win7和Win8用户该选项无法使用) - 启用ForceFullScreen - # 当检测到窗口大小变化时,自动使用Win32API将本窗口的大小设置为主显示器大小(设备像素大小),不需要可以关闭,实时生效。 - 启用DPIChangeDetection - # 当检测到系统DPI变化时,会尝试检测FloatingBar是否在屏幕内显示,如果不在屏幕内显示将会尝试移动到屏幕内可见区域(DPI调大会触发,如果DPI调小是不会触发工具栏位置移动的,请您手动调整)。 - 启用ResolutionChangeDetection - # 当检测到系统分辨率变化时,会尝试检测FloatingBar是否在屏幕内显示,如果不在屏幕内显示将会尝试移动到屏幕内可见区域(分辨率调小可能会触发,如果在屏幕内不会自动调整位置,请手动挡)。 - 希沃白板3 - 希沃白板5 - 希沃白板5C - 希沃品课 - 希沃品课画笔 - 希沃PPT小工具 - AiClass - 鸿合屏幕书写 - 畅言智慧课堂 - 畅言PPT - 天喻教育云 - 希沃桌面画笔 - 希沃桌面侧栏 - 多指书写 - 双指移动 - 双指缩放 - 双指旋转 - 背景 - 选择 - - 荧光笔 - 橡皮 - 橡皮选项 - 大小 - 橡皮形状 - 圆形擦 - 黑板擦 - 清空墨迹 - 清空墨迹和历史 - 墨迹擦 - 图形 - (第一行支持长按保持选中) - 自动隐藏 - 插入图片 - 选择图片 - 截图 - 撤销 - 重做 - 工具 - 退出 - 新页面 - 上一页 - 下一页 - 页面 - 删除此页 - 测试文本 - 退出 - 粗细 - 深色 - 背景 - 隐藏 画板 - 检查 - 从头 放映 - 结束 放映 - 单指 拖动 - 恢复 - 清屏 & 隐藏 - 鼠标 - 批注 - 清空 - 视频展台 - 拍摄照片 - 摄像头设备 - 上屏 - 矫正 - 拍照 - 旋转 - 界面语言 - 跟随系统 - 简体中文 - English - 更改界面语言后需要重启应用程序才能完全生效。 - 面积擦 - 线擦 - 套索选 - 几何 - 清并鼠 - 白板 - 隐藏 - 几何绘图 - 画直线 - 画虚线 - 画点线 - 画箭头 - 4平行线 - 中心正方 - 中心圆 - 中心虚圆 - 中心椭圆 - 长方体 - 正方形 - 画圆柱 - 画圆锥 - 手势 - 手势选项 - 多指书写 - 双指移动 - 双指缩放 - 双指旋转 - 手势 - 手势选项 - + + 手势设置 + + + 个性化设置 + + + PPT设置 + + + 高级设置 + + + 自动化设置 + + + 随机窗口设置 + + + 快捷键设置 + + + 折叠侧边栏 + + + 显示侧边栏 + + + 通过 icc:// 协议从外部控制软件 + + + 设置 + + + 设置更改将自动保存,部分设置需要重启软件后生效 + + + 重启 + + + 重置 + + + 退出 + + + 模式设置 + + + 选择软件运行模式。仅PPT模式下,软件将完全隐藏,仅在PPT放映时出现。(实验性功能,可能不稳定。) + + + 运行模式 + + + 正常模式 + + + 仅PPT模式 + + + 新设置窗口 + + + 打开新的设置窗口,提供更丰富的设置选项和更好的用户体验。(开发中) + + + 打开新设置窗口 + + + 插件管理 + + + 通过插件扩展InkCanvas的功能。您可以启用或禁用插件,或加载自定义插件。 + + + 打开插件管理器 + + + 启动 + + + 窗口无焦点模式 + + + 窗口无边框模式 + + + 窗口置顶 + + + UIA置顶 + + + # 开启UIA置顶后,软件需要管理员启动才能置顶… + + + 自动检查更新 + + + 静默更新 + + + # 静默更新将在软件不使用时自动安装,无需手动操作 + + + 更新通道 + + + 稳定版 (Release) + + + 预览版 (Preview) + + + 测试版 (Beta) + + + # 稳定版提供可靠更新,预览版提供新功能体验… + + + 手动更新 + + + # 点击后立即检查并下载最新版本 + + + 版本修复 + + + # 版本修复会根据当前选择的通道下载最新版本并执行安装… + + + 历史版本回滚 + + + # 历史版本回滚,点击后会弹出相应页面… + + + # 关闭静默更新后,已完成安装包的下载后将会弹窗询问… + + + 静默更新时间段 + + + 起始时间 + + + 终止时间 + + + # 若终止时间小于起始时间… + + + 开机时运行 + + + 开机运行后收纳到侧边栏 + + + 画板和墨迹 + + + 显示画笔光标 + + + 启用压感触屏模式 + + + # 开启后,触屏设备也将支持压感效果… + + + 屏蔽压感 + + + # 开启后,将忽略所有设备的压感信息… + + + 橡皮大小 + + + 很小 + + + 较小 + + + 中等 + + + 较大 + + + 很大 + + + # 非实时切换,下一次使用面积擦时生效。 + + + 退出画板模式后隐藏墨迹 + + + # 开启 退出画板模式后隐藏墨迹 选项后… + + + 清空墨迹时删除墨迹历史记录 + + + 清空画布时同时清空图片 + + + 插入图片时自动压缩(大于1920x1080) + + + 保留双曲线渐近线 + + + + + + + + + 每次询问 + + + # 请注意,若不保留双曲线渐近线可能会有遇到撤回相关的 BUG… + + + 绘制圆时显示圆心位置 + + + 使用WPF默认贝塞尔曲线平滑 + + + 使用高级曲线平滑(推荐) + + + 启用墨迹渐隐功能 + + + # 开启后墨迹不会绘制到画布上… + + + 墨迹渐隐时间 + + + 在笔工具菜单中隐藏墨迹渐隐控制 + + + # 开启后,主工具栏上点击笔工具后弹出的上下文菜单中将不显示… + + + 颜色 + + + 默认 + + + 黑色 + + + 白色 + + + 红色 + + + 黄色 + + + 蓝色 + + + 绿色 + + + 橙色 + + + 紫色 + + + 更新已下载完成,将在软件关闭时自动安装。 + + + 更新已准备就绪 + + + 更新下载失败,请检查网络连接后重试。 + + + 下载失败 + + + 已设置跳过版本 {0},在下次发布新版本之前不会再提示更新。 + + + 已跳过此版本 + + + 抱歉,出现未预期的异常,可能导致 InkCanvasForClass 运行不稳定。\n建议保存墨迹后重启应用。 + + + 重启次数过多 + + + 检测到程序已连续重启5次,已停止自动重启。请联系开发者或检查系统环境。 + + + 正在启动 Ink Canvas... + + + 崩溃后操作 + + + 请选择软件发生未处理异常时的自动操作: + + + 静默重启软件 + + + 无操作 + + + # 静默重启:崩溃后自动重启软件,无提示。无操作:崩溃后仅记录日志,不自动重启。 + + + 手势 + + + 进退白板模式自动开关双指移动功能 + + + # 开启后退出画板模式时自动关闭双指移动手势,进入白板模式时自动开启双指移动手势 + + + 允许双指旋转与缩放选中的墨迹 + + + # 允许选中墨迹后对墨迹进行双指或多指缩放操作(此设置不受“允许双指旋转”设置的影响) + + + 启用手掌擦 + + + 手掌擦敏感度 + + + 低敏感度 + + + 中敏感度 + + + 高敏感度 + + + # 低敏感度:需要更大的触摸面积和更多触摸点,减少误判;高敏感度:更容易触发手掌擦,但可能误判手指。 + + + 墨迹纠正 + + + 启用墨迹识别 + + + 识别引擎 + + + 自动:64 位进程使用 WinRT(Windows 10+),32 位使用 IACore。可强制指定 IACore 或 WinRT。 + + + 自动 + + + IACore + + + WinRT + + + 阻止矫正后的矩形带有模拟压感值 + + + 阻止矫正后的三角形带有模拟压感值 + + + 矫正手绘三角形 + + + 矫正手绘矩形 + + + 矫正手绘圆形与椭圆 + + + 直线自动拉直 + + + 长度阈值 + + + 灵敏度 + + + 高精度直线拉直 + + + # 开启后,当绘制的直线超过设定长度阈值时,将自动调整为完美直线。灵敏度范围0.05-2.0,越小要求越严格,越弯曲的线条越不容易被拉直;值越大越容易识别为直线。高精度模式下,每隔10像素取一个计数点,获取更准确的平均值用于判断。 + + + 直线端点吸附 + + + 吸附距离 + + + 个性化 + + + 主题 + + + 浅色主题 + + + 深色主题 + + + 跟随系统 + + + 启用启动动画 + + + 启动动画样式 + + + 随机 + + + 跟随四季 + + + 春季 + + + 夏季 + + + 秋季 + + + 冬季 + + + 马年限定 + + + 浮动工具栏图标 + + + “ICC-CE”默认 + + + “ICC-CE”无阴影 + + + “ICC-CE”深色 + + + “ICC-CE”深色呼吸版 + + + “ICC-CE”白色透明版 + + + “ICC-CE”黑色透明版 + + + 酷安斗鸡眼滑稽 + + + 酷安受虐滑稽 + + + 酷安呲牙笑 + + + 酷安头戴内裤滑稽 + + + 酷安绿帽Doge + + + 贴吧滑稽 + + + 自定义浮动栏图标 + + + 上传 + + + 管理 + + + 浮动工具栏缩放 + + + 浮动工具栏透明度 + + + 浮栏在PPT下透明度 + + + # 重新进入PPT放映后生效 + + + 在调色盘窗口中显示 笔尖模式 按钮 + + + 白板 UI 80% 缩放 + + + 在白板中显示当前时间和日期 + + + 在白板中显示信仰の源1(好喝的/毒的鸡汤) + + + 信仰の源出自Where? + + + osu!玩家语录 + + + 励志立志的名言警句 + + + 高考祝福语 + + + 一言(Hitokoto API) + + + 自定义 + + + 在收纳模式下启用快速面板 + + + 取消收纳按钮图标 + + + 箭头 + + + + + + 浮动栏按钮显示 + + + 使用老版浮动栏按钮UI + + + 显示形状按钮 + + + 显示撤销按钮 + + + 显示重做按钮 + + + 显示清空按钮 + + + 显示白板按钮 + + + 显示隐藏按钮 + + + 显示套索选择按钮 + + + 显示清并鼠按钮 + + + 显示快捷调色盘 + + + 快捷调色盘显示模式 + + + 单行显示(6色) + + + 双行显示(8色) + + + 橡皮按钮显示 + + + 两个都显示 + + + 仅显示面积擦 + + + 仅显示线擦 + + + 都不显示 + + + 任务栏托盘图标 + + + 启用托盘图标 + + + PPT联动 + + + 此部分的设置项将会在幻灯片播放时使用,优先级高于其他设置项 + + + Microsoft PowerPoint 支持 + + + PowerPoint 联动增强 + + + 抢占焦点放映(用于跳过PPT动画) + + + 使用 ROT 联动 + + + WPS 支持 + + + WPP进程查杀(防止WPP残留进程) + + + # 关闭后将不会自动查杀WPP残留进程,可能导致WPP关闭卡顿或无法彻底退出。 + + + # 如果您只使用PowerPoint请不要打开WPS联动开关,如果使用WPS建议不要使用PowerPoint! + + + 开启WPS支持后会导致WPS关闭时卡顿! + + + # 可支持 WPS,但目前无法同时支持 MSOffice 和 WPS。若要启用WPS支持,请确保 WPS 是否在 “配置工具” 中开启了 “WPS Office 兼容第三方系统和软件” 选项,否则将无法识别到WPS! + + + 退出画板模式后隐藏墨迹 + + + # 开启 退出画板模式后隐藏墨迹 选项后,进入 PPT 模式时未处于批注模式时不会显示墨迹。 + + + 清空墨迹时删除墨迹历史记录 + + + 清空画布时同时清空图片 + + + 插入图片时自动压缩(大于1920x1080) + + + PPT翻页按钮 + + + 在 PPT 模式下显示翻页按钮 + + + 左下 + + + 右下 + + + 左侧 + + + 右侧 + + + 左侧偏移 + + + 左侧透明度 + + + 右侧偏移 + + + 右侧透明度 + + + # 调大往上偏移,调小往下偏移,修改为0为不偏移,居中放置 + + + 左下偏移 + + + 左下透明度 + + + 右下偏移 + + + 右下透明度 + + + # 调大往右偏移,调小往左偏移,修改为0为不偏移,居中放置 + + + 两侧 + + + 显示页码 + + + 半透明 + + + 黑色背景 + + + 左下右下 + + + PPT 页码按钮可点击 + + + # 开启该选项后,点击页码按钮可以唤起PowerPoint自带的网格缩略图视图。WPS不支持该功能,开启也没用。 + + + PPT 翻页按钮长按翻页 + + + # 开启该选项后,长按PPT翻页按钮可以连续翻页,提高翻页效率。 + + + # 开启UIA置顶后,软件需要管理员启动才能置顶,关闭此功能需要完全关闭软件后再手动启动,无法使用重启来关闭此功能 + + + # 静默更新将在软件不使用时自动安装,无需手动操作 + + + 更新通道 + + + 稳定版 (Release) + + + 预览版 (Preview) + + + 测试版 (Beta) + + + 软件架构 + + + 32 位 (x86) + + + 64 位 (x64) + + + # 选择要下载架构 + + + # 稳定版提供可靠更新,预览版提供新功能体验同时拥有相较Beta版更强的稳定性,测试版提供新功能抢先体验 + + + # 点击后立即检查并下载最新版本 + + + 版本修复 + + + # 版本修复会根据当前选择的通道下载最新版本并执行安装,可用于修复损坏的安装 + + + 历史版本回滚 + + + # 历史版本回滚,点击后会弹出相应页面供用户手动回滚到之前的版本 + + + # 关闭静默更新后,已完成安装包的下载后将会弹窗询问是否进行更新,开启静默更新后将会在安装包下载完成后每隔十分钟进行如下检测:①处于静默更新时间段内 ②未处于书写模式 ③未处于画板内。若以上检测通过即会关闭软件进行自动更新。 + + + 静默更新时间段 + + + 起始时间 + + + 终止时间 + + + # 若终止时间小于起始时间,即将终止时间视为第二天的时间。# 若起始时间与终止时间相同,即视为全天候时间。 + + + 开机时运行 + + + 开机运行后收纳到侧边栏 + + + 画板和墨迹 + + + 显示画笔光标 + + + 启用压感触屏模式 + + + # 开启后,触屏设备也将支持压感效果,适用于部分支持压感但无法被系统识别的触屏设备。 + + + 屏蔽压感 + + + # 开启后,将忽略所有设备的压感信息,使所有笔画具有统一的粗细。与压感触屏模式互斥。 + + + 很小 + + + 较小 + + + 中等 + + + 较大 + + + 很大 + + + # 非实时切换,下一次使用面积擦时生效。 + + + 保留双曲线渐近线 + + + + + + + + + 每次询问 + + + # 请注意,若不保留双曲线渐近线可能会有遇到撤回相关的 BUG 影响用。 + + + 使用WPF默认贝塞尔曲线平滑 + + + 使用高级曲线平滑(推荐) + + + 启用墨迹渐隐功能 + + + # 开启后墨迹不会绘制到画布上,而是保持湿墨迹状态,根据设置的渐隐时间自动消失 + + + 在笔工具菜单中隐藏墨迹渐隐控制 + + + # 开启后,主工具栏上点击笔工具后弹出的上下文菜单中将不显示墨迹渐隐控制开关 + + + 启用画笔自动恢复 + + + # 启用后,临时修改画笔设置后将在指定时间点自动恢复到你在此处配置的颜色 / 透明度 / 粗细 + + + 自动恢复时间点 (HH:mm,可多个,用 ; 分隔) + + + 恢复目标颜色 + + + 默认 + + + 黑色 + + + 白色 + + + 红色 + + + 黄色 + + + 蓝色 + + + 绿色 + + + 橙色 + + + 紫色 + + + 恢复笔粗细 + + + 恢复透明度 + + + 使用橡皮擦后自动切换回批注模式 + + + # 开启后,使用橡皮擦进行擦除操作后静置一段时间将自动切换回批注模式 + + + 自动切换延迟时间 + + + # 若在计时时间内再次进行擦除操作,计时器将重新开始计时 + + + # 开启后,当绘制的直线端点靠近其他直线端点时,将自动吸附连接。 + + + 进入 PPT 放映时自动进入批注模式 + + + 该项与"自动化"中的"播放PPT时自动收纳"选项冲突! + + + 允许幻灯片模式下的双指手势 + + + 允许使用手指手势进行幻灯片翻页 + + + # 允许开启画板时使用手指手势进行幻灯片翻页(启用后,在幻灯片放映模式下,当画板无墨迹时,使用手指(笔尖或手掌无法识别)左右滑动即可控制幻灯片翻页。) + + + PPT 放映模式显示手势按钮 + + + # 开启后在 PPT 放映模式下也显示手势按钮 + + + PPT时间显示胶囊 + + + # 开启后在 PPT 放映模式下显示时间胶囊,可替代最小化计时器窗口 + + + 时间胶囊位置: + + + 左上角 + + + 右上角 + + + 顶部居中 + + + PPT 放映时显示快速面板 + + + # 关闭后在 PPT 放映时不显示快速面板 + + + 自动幻灯片截屏 + + + # 开启 自动幻灯片截屏 后将会在幻灯片有墨迹时翻页自动截屏 + + + 自动保存幻灯片墨迹 + + + # 开启 自动保存幻灯片墨迹 后将在结束幻灯片放映时自动将保存已有墨迹,并在下次打开时自动加载(文件名和幻灯片页数都要相同) + + + 记忆并提示上次播放位置 + + + #开启后会记录上次播放的页数,点击"是"后会自动跳转 + + + 进入放映时回到首页 + + + 提示隐藏幻灯片 + + + 提示是否已启用自动播放 + + + 高级设置 + + + 可在手指触摸画板时显示圆形橡皮或手掌触摸画板时显示的橡皮比 +手掌大很多时调整 + + + 特殊屏幕模式 + + + 触摸倍数 + + + 在下方区域内用笔尖点击以估计触摸大小倍数 + + + # 数值仅供参考 + + + 橡皮擦绑定触摸大小倍数 + + + # BoundsWidth 参数作为接触面积区分界限 + + + 四边红外模式 + + + 记录日志 + + + 日志以日期保存 + + + # 日志文件超过 512 KB 时会自动删除。开启日期保存后,日志将保存在Logs文件夹中,当文件夹大小超过5MB时自动清空。 + + + 关闭软件时二次弹窗确认 + + + 启用FullScreenHelper + + + 实验性选项 + + + # 感谢lindexi大佬提供的FullScreenHelper,可以减少任务栏弹出的问题,且支持多显示器自动全屏(虽然对icc来说没什么用就是了),如果遇到一些玄学问题,可以关闭该功能,重启icc后生效。 + + + 启用AvoidFullScreenHelper + + + # 避免画布全屏,应该可解决任务栏非置顶和Win11任务栏无法点击的问题,会导致左侧或顶部有AppBar(Dock栏软件)时导致浮动工具栏偏移,重启icc后生效。 + + + 启用EdgeGestureUtil + + + 更多功能 + + + 计时器 + + + 随机抽 + + + 单次抽选 + + + 保存 + + + 打开... + + + 重播 + + + 截屏 + + + 说明书 + + + 设置 + + + 单次抽 + + + 随机抽 + + + 计时器 + + + 白板 + + + 退出放映 + + + 批注栏 + + + 退出 + + + 设置备份与还原 + + + # 可手动备份当前设置或还原之前的备份,自动更新前也会自动备份 + + + 自动更新前备份 + + + 定期自动备份 + + + 备份间隔 + + + 1天 + + + 3天 + + + 7天 + + + 14天 + + + 30天 + + + (默认7天) + + + 手动备份 + + + 还原备份 + + + 配置文件切换与热重载 + + + # 选择配置文件即切换并热重载;另存为可将当前配置保存为新配置文件 + + + 配置文件: + + + 删除配置文件 + + + 另存为配置文件 + + + 自动化 + + + 自动收纳 + + + 希沃白板5 + + + 希沃展台 + + + 希沃白板3 + + + 希沃轻白板 + + + 希沃轻白板5C + + + 希沃品课 + + + 鸿合白板 + + + 鸿合展台 + + + 鸿合轻量白板 + + + 文香白板 + + + 微软白板 + + + 安道白板 + + + 安道展台 + + + 艺云白板 + + + 艺云展台 + + + MaxHub白板 + + + 自动收纳忽略桌面EN5批注窗口 + + + 进入“中原旧白板”时自动收纳 + + + 播放PPT时自动收纳 + + + 软件退出后保持收纳模式 + + + # 开启后,执行自动收纳的软件在软件退出后不退出收纳模式,保持收纳状态 + + + 自动查杀 + + + 自动查杀希沃“PPT 小工具” + + + # 请注意,查杀 PPT 小工具会导致希沃课堂授课助手无法使用,直接进入希沃课堂授课助手安装目录删除 Office.dll 文件即可进入 PPT 放映时不会启动希沃的 PPT 工具栏。 + + + 自动查杀 希沃白板5 + + + 自动查杀 鸿合屏幕书写 + + + 鸿合屏幕书写查杀后自动进入批注 + + + 自动查杀 优教授课端 + + + 自动查杀 希沃桌面2.0 桌面批注 + + + # 由于希沃桌面2.0提供的桌面批注是64位应用程序,icc是32位程序无法访问,所以目前暂不做精准匹配,只匹配进程名称DesktopAnnotation,后面会考虑封装一套基于P/Invoke和WMI的综合进程识别方案。如果遇到同进程名的软件直接不开启该选项就行了,见谅! + + + 同类软件查杀 + + + 自动查杀 Ink Canvas 和 IC+ + + + 自动查杀ICA(新版旧版通杀) + + + 自动查杀 智绘教Inkeys(仅限新版) + + + 文件关联管理 + + + 管理.icstk文件的关联设置,双击.icstk文件可直接在Ink Canvas中打开 + + + 取消文件关联 + + + 检查关联状态 + + + 重新注册关联 + + + 悬浮窗拦截 + + + 自动检测并拦截同类软件的悬浮窗 + + + 启用悬浮窗拦截 + + + 拦截器未启动 + + + 清屏时自动截图 + + + 截图分日期文件夹保存 + + + 截图时自动保存墨迹 + + + 定时自动保存墨迹 + + + 保存间隔 + + + 1分钟 + + + 3分钟 + + + 5分钟 + + + 10分钟 + + + 15分钟 + + + 30分钟 + + + 60分钟 + + + # 开启后将在设定时间间隔自动保存墨迹,仅在画布可见且有墨迹时才会保存 + + + 墨迹全页面保存 + + + # 开启后自动保存和手动保存墨迹时将以全屏模式保存。如果存在多个画布和墨迹,将把所有页面的墨迹按照每页为单位保存进一个压缩包中(注意,白板的墨迹只能在白板模式下打开,PPT的墨迹只能在PPT放映模式下打开) + + + 保存为XML格式 + + + # 开启后保存墨迹时将使用XML格式(ISF格式),便于查看和编辑墨迹数据 + + + 自动截图最小墨迹量 + + + 墨迹与截图的保存路径 + + + 浏览 + + + 设置保存到 D:\Ink Canvas + + + 设置保存到 文档 + + + # 请注意检查保存文件夹是否有写入权限 + + + 定期自动删除超过保存时间的墨迹、截图文件 + + + # 请注意如果开启自动删除功能,将会删除自动保存目录下所有后缀名为 .icstk 和 .png 的文件! + + + 保存时长 + + + + + + 云存储管理 + + + 收纳模式 + + + 退出收纳模式时自动切换至批注模式 + + + # 开启后,退出收纳模式时将自动切换至批注模式,便于快速批注 + + + 退出PPT放映后自动收纳浮动栏 + + + # 开启后,退出PPT放映后会自动收纳浮动栏 + + + 退出白板时自动收纳 + + + # 开启后,退出白板模式时会自动收纳到侧边栏 + + + 随机点名 + + + 显示修改随机点名名单的按钮 + + + 点名窗口背景设置(仅老版点名UI有效) + + + 背景选择: + + + 默认背景 + + + 自定义背景: + + + 上传 + + + 管理 + + + 启用随机抽和单次抽按钮 + + + 启用快抽悬浮按钮 + + + 直接调用外部点名 + + + 点名类型 + + + ClassIsland点名 + + + SecRandom点名 + + + NamePicker点名 + + + 单次抽人窗口关闭延迟 + + + 单次随机点名人数上限 + + + 新点名UI设置 + + + 启用新点名UI + + + 启用机器学习避免重复 + + + 避免重复历史记录数量 + + + 避免重复权重 + + + # 机器学习算法会分析最近的点名历史,智能避免重复选择相同人员 + + + 计时器设置 + + + 使用老版计时器按钮UI + + + 新计时器UI + + + 启用正计时 + + + 超时醒目数字 + + + 计时器提醒音量 + + + 自定义提醒铃声: + + + 选择文件 + + + 重置 + + + 渐进提醒 + + + 渐进提醒音量 + + + 自定义渐进提醒音频: + + + 选择文件 + + + 重置 + + + 关于 + + + 设备信息 + + + 设备ID: + + + 使用频率: + + + 更新优先级: + + + 启动次数: + + + 总使用时长: + + + 正在获取... + + + 刷新设备信息 + + + 我已阅读并同意 + + + 中的隐私说明 + + + 匿名使用数据上传: + + + 关闭(不上传) + + + 上传基础数据 + + + 上传基础 + 可选数据 + + + # 使用和分发本软件前,请您应当且务必知晓相关开源协议,且您应当知晓本软件基于 https://github.com/WXRIW/Ink-Canvas 修改而成。 + + + 本软件和ICA,Ink Canvas均基于许可证开源 + + + 本强许可协议的许可条件是,在相同许可协议下,提供许可作品的完整源代码和修改,包括使用许可作品的大型作品。版权和许可声明必须保留。贡献者明确授予专利权。 + + + 开发者: + + + ICC CE 的开发者 + + + ICC 的开发者 + + + ICA 的开发者 + + + Ink Canvas 的开发者 + + + ICC 仓库源: + + + ICA 仓库源: + + + Ink Canvas 仓库源: + + + 感谢下列贡献者: + + + © 2025-2026 CJK_mkp 版权所有 + + + 我们永远热爱开源! + + + 版本: + + + 关闭 + + + + + + + + + 外部协议调用 (icc://) + + + 笔尖模式 BoundsWidth + + + 手指模式 BoundsWidth + + + # EdgeGestureUtil是icc最新引入的可以暂时阻止在使用触摸时触发边缘手势(如Windows10环境下,屏幕左边缘滑动进入任务视图,右边缘滑动弹出通知中心;Windows11环境下,底部向上滑动打开开始菜单),其原理是使用了 + + + (当应用程序窗口处于活动状态且处于全屏模式 (或拥有的窗口) 处于活动状态时,防止边缘手势行为。)来实现的。如果有异常,请关闭该选项,该选项应该能够实时生效。(Win7和Win8用户该选项无法使用) + + + 启用ForceFullScreen + + + # 当检测到窗口大小变化时,自动使用Win32API将本窗口的大小设置为主显示器大小(设备像素大小),不需要可以关闭,实时生效。 + + + 启用DPIChangeDetection + + + # 当检测到系统DPI变化时,会尝试检测FloatingBar是否在屏幕内显示,如果不在屏幕内显示将会尝试移动到屏幕内可见区域(DPI调大会触发,如果DPI调小是不会触发工具栏位置移动的,请您手动调整)。 + + + 启用ResolutionChangeDetection + + + # 当检测到系统分辨率变化时,会尝试检测FloatingBar是否在屏幕内显示,如果不在屏幕内显示将会尝试移动到屏幕内可见区域(分辨率调小可能会触发,如果在屏幕内不会自动调整位置,请手动挡)。 + + + 希沃白板3 + + + 希沃白板5 + + + 希沃白板5C + + + 希沃品课 + + + 希沃品课画笔 + + + 希沃PPT小工具 + + + AiClass + + + 鸿合屏幕书写 + + + 畅言智慧课堂 + + + 畅言PPT + + + 天喻教育云 + + + 希沃桌面画笔 + + + 希沃桌面侧栏 + + + 多指书写 + + + 双指移动 + + + 双指缩放 + + + 双指旋转 + + + 背景 + + + 选择 + + + + + + 荧光笔 + + + 橡皮 + + + 橡皮选项 + + + 大小 + + + 橡皮形状 + + + 圆形擦 + + + 黑板擦 + + + 清空墨迹 + + + 清空墨迹和历史 + + + 墨迹擦 + + + 图形 + + + (第一行支持长按保持选中) + + + 自动隐藏 + + + 插入图片 + + + 选择图片 + + + 截图 + + + 撤销 + + + 重做 + + + 工具 + + + 退出 + + + 新页面 + + + 上一页 + + + 下一页 + + + 页面 + + + 删除此页 + + + 测试文本 + + + 退出 + + + 粗细 + + + 深色 + + + 背景 + + + 隐藏 +画板 + + + 检查 + + + 从头 +放映 + + + 结束 +放映 + + + 单指 +拖动 + + + 恢复 + + + 清屏 +& +隐藏 + + + 鼠标 + + + 批注 + + + 清空 + + + 视频展台 + + + 拍摄照片 + + + 摄像头设备 + + + 上屏 + + + 矫正 + + + 拍照 + + + 旋转 + + + 界面语言 + + + 跟随系统 + + + 简体中文 + + + English + + + 更改界面语言后需要重启应用程序才能完全生效。 + + + 面积擦 + + + 线擦 + + + 套索选 + + + 几何 + + + 清并鼠 + + + 白板 + + + 隐藏 + + + 几何绘图 + + + 画直线 + + + 画虚线 + + + 画点线 + + + 画箭头 + + + 4平行线 + + + 中心正方 + + + 中心圆 + + + 中心虚圆 + + + 中心椭圆 + + + 长方体 + + + 正方形 + + + 画圆柱 + + + 画圆锥 + + + 手势 + + + 手势选项 + + + 多指书写 + + + 双指移动 + + + 双指缩放 + + + 双指旋转 + + + 手势 + + + 手势选项 + + + 窗口失去焦点时仍可接收输入 + + + 隐藏窗口边框,提供更沉浸的体验 + + + 窗口始终显示在其他窗口之上 + + + 系统启动时自动运行软件 + + + 开机启动后自动收纳到侧边栏 + + + 启用后,软件将完全隐藏,仅在PPT放映时出现 + + + 启用特殊屏幕模式以优化触摸体验 + + + 调整触摸大小的倍数 + + + 橡皮擦大小与触摸大小倍数绑定 + + + 笔尖模式的接触面积区分界限 + + + 手指模式的接触面积区分界限 + + + 启用四边红外触摸屏模式 + + + 启用日志记录功能 + + + 关闭软件时显示确认对话框 + + + 在自动更新前自动备份当前设置 + + + 定期自动备份设置文件 + + + 设置定期备份的时间间隔 + + + 开启后可通过 icc://fold (收纳) 或 icc://unfold (展开) 等命令控制 + + + 开启后自动检测并拦截同类软件的悬浮窗 + + + 选择应用程序的主题样式 + + + 启动时显示动画效果 + + + 选择启动动画的样式 + + + 选择浮动工具栏的图标样式 + + + 调整浮动工具栏的大小 + + + 调整浮动工具栏的透明度 + + + 在PPT模式下浮动工具栏的透明度(重新进入PPT放映后生效) + + + 在调色盘窗口中显示笔尖模式切换按钮 + + + 使用旧版浮动栏按钮的界面样式 + + + 在浮动栏中显示形状按钮 + + + 在浮动栏中显示撤销按钮 + + + 在浮动栏中显示重做按钮 + + + 在浮动栏中显示清空按钮 + + + 在浮动栏中显示白板按钮 + + + 在浮动栏中显示隐藏按钮 + + + 在浮动栏中显示套索选择按钮 + + + 在浮动栏中显示清并鼠按钮 + + + 在浮动栏中显示快捷调色盘 + + + 选择快捷调色盘的显示模式 + + + 在任务栏系统托盘显示图标 + + + 将白板界面缩放至80% + + + 在白板界面显示当前时间和日期 + + + 在白板界面显示励志语句 + + + 在收纳模式下显示快速操作面板 + + + 选择取消收纳按钮的图标样式 + + + 开启后,退出收纳模式时将自动切换至批注模式,便于快速批注 + + + 开启后,退出PPT放映后会自动收纳浮动栏 + + + 开启后,退出白板模式时会自动收纳到侧边栏 + + + 开启后,执行自动收纳的软件在软件退出后不退出收纳模式,保持收纳状态 + + + 查杀鸿合屏幕书写后自动进入批注模式 + + + 绘制时显示画笔光标 + + + 开启后,触屏设备也将支持压感效果,适用于部分支持压感但无法被系统识别的触屏设备 + + + 开启后,将忽略所有设备的压感信息,使所有笔画具有统一的粗细。与压感触屏模式互斥 + + + 开启后,进入 PPT 模式时未处于批注模式时不会显示墨迹 + + + 开启后,清空墨迹后将无法通过"撤销"功能恢复 + + + 清空画布时同时删除所有插入的图片 + + + 插入的图片大于1920x1080时自动压缩以节省内存 + + + 绘制圆形时显示圆心标记 + + + 使用WPF内置的曲线平滑算法 + + + 使用改进的贝塞尔曲线平滑算法,提供更好的绘制效果 + + + 开启后墨迹不会绘制到画布上,而是保持湿墨迹状态,根据设置的渐隐时间自动消失 + + + 设置墨迹从绘制到完全消失的时间 + + + 开启后,主工具栏上点击笔工具后弹出的上下文菜单中将不显示墨迹渐隐控制开关 + + + 开启后将在设定时间间隔自动保存墨迹,仅在画布可见且有墨迹时才会保存 + + + 开启后保存墨迹时将使用XML格式(ISF格式),便于查看和编辑墨迹数据 + + + 开启后退出画板模式时自动关闭双指移动手势,进入白板模式时自动开启双指移动手势 + + + 允许选中墨迹后对墨迹进行双指或多指缩放操作 + + + 开启后,两个及以上触点且触摸面积较大时自动切换为橡皮擦,抬手后恢复原编辑模式 + + + 自动将手绘图形识别并转换为标准形状 + + + 矫正后的矩形不包含模拟压感信息 + + + 矫正后的三角形不包含模拟压感信息 + + + 当绘制的直线超过设定长度阈值时,将自动调整为完美直线 + + + 高精度模式下,每隔10像素取一个计数点,获取更准确的平均值用于判断 + + + 当绘制的直线端点靠近其他直线端点时,将自动吸附连接 + + + 在随机点名窗口中显示修改名单的按钮 + + + 在随机点名窗口中显示随机抽和单次抽按钮 + + + 启用快速抽人的悬浮按钮 + + + 使用外部点名工具代替内置点名功能 + + + 设置单次抽人窗口自动关闭的延迟时间 + + + 设置单次随机点名最多抽取的人数 + + + 使用新的点名界面设计 + + + 使用机器学习算法避免重复抽取相同的人 + + + 设置用于避免重复的历史记录数量 + + + 设置机器学习算法避免重复的权重 + + + 选择点名窗口的背景 + + + 在PPT放映模式下显示翻页控制按钮 + + + 调大往上偏移,调小往下偏移,修改为0为不偏移,居中放置 + + + 调大往右偏移,调小往左偏移,修改为0为不偏移,居中放置 + + + 调整左下按钮的位置 + + + 调整右下按钮的位置 + + + 调整左侧翻页按钮的透明度 + + + 调整右侧翻页按钮的透明度 + + + 调整左下翻页按钮的透明度 + + + 调整右下翻页按钮的透明度 + + + 进入PPT放映时自动切换到批注模式 + + + 在幻灯片模式下允许使用双指手势 + + + 开启后在 PPT 放映模式下也显示手势按钮 + + + 选择时间胶囊在屏幕上的显示位置 + + + 开启后会记录上次播放的页数,点击"是"后会自动跳转 + + + 进入PPT放映时自动跳转到第一页 + + + 在播放隐藏幻灯片时显示提示 + + + 在PPT自动播放时显示提示 + + + 开启后需要设置密码 + + + 设置或修改安全密码 + + + 关闭 InkCanvasForClass 时进行密码验证 + + + 打开设置面板/新设置窗口时进行验证 + + + 重置 Settings.json 前进行二次验证 + + + 在修改名单或清空名单前进行密码验证 + + + 开启后关键文件会被只读占用 + + + {Binding Description} + + + 清空画布时自动保存当前画布截图 + + + 截图按日期分类保存到不同文件夹 + + + 截图时同时保存当前画布的墨迹数据 + + + 开启后,在幻灯片有墨迹时翻页会自动截屏 + + + 设置自动截图时所需的最小墨迹量 + + + 开启后将会删除自动保存目录下所有后缀名为 .icstk 和 .png 的文件 + + + 选择应用程序的主题样式 + + + 启动时显示动画效果 + + + 选择启动动画的样式 + + + 选择浮动工具栏的图标样式 + + + 调整浮动工具栏的大小 + + + 调整浮动工具栏的透明度 + + + 在PPT模式下浮动工具栏的透明度(重新进入PPT放映后生效) + + + 在调色盘窗口中显示笔尖模式切换按钮 + + + 使用旧版浮动栏按钮的界面样式 + + + 在浮动栏中显示形状按钮 + + + 在浮动栏中显示撤销按钮 + + + 在浮动栏中显示重做按钮 + + + 在浮动栏中显示清空按钮 + + + 在浮动栏中显示白板按钮 + + + 在浮动栏中显示隐藏按钮 + + + 在浮动栏中显示套索选择按钮 + + + 在浮动栏中显示清并鼠按钮 + + + 在浮动栏中显示快捷调色盘 + + + 选择快捷调色盘的显示模式 + + + 在任务栏系统托盘显示图标 + + + 将白板界面缩放至80% + + + 在白板界面显示当前时间和日期 + + + 在白板界面显示励志语句 + + + 在收纳模式下显示快速操作面板 + + + 选择取消收纳按钮的图标样式 + + + 开启后,退出收纳模式时将自动切换至批注模式,便于快速批注 + + + 开启后,退出PPT放映后会自动收纳浮动栏 + + + 开启后,退出白板模式时会自动收纳到侧边栏 + + + 使用旧版本的计时器按钮界面 + + + 使用新样式的计时器界面 + + + 计时器超时后继续正向计时 + + + 计时器超时后数字显示为红色 + + + 设置计时器提醒音的音量 + + + 启用渐进式提醒功能 + + + 设置渐进提醒音的音量 + + + 选择要回滚到的历史版本 + + + 回滚到下拉框中选择的版本 + + + 根据当前选择的通道下载最新版本并执行安装,可用于修复损坏的安装 + + + 定期检查是否有新版本可用 + + + 在软件不使用时自动安装更新,无需手动操作 + + + 使用UIA方式实现置顶,需要管理员权限 + + + 白板展台按钮启动希沃视频展台 + + + 开启后,点击白板工具栏「展台」将打开希沃视频展台(需已安装);关闭则使用内置展台。 + + \ No newline at end of file diff --git a/Ink Canvas/Windows/HotkeyItem.xaml b/Ink Canvas/Windows/HotkeyItem.xaml index 0f2ff92a..3cfc5b1e 100644 --- a/Ink Canvas/Windows/HotkeyItem.xaml +++ b/Ink Canvas/Windows/HotkeyItem.xaml @@ -1,66 +1,70 @@ - - - + mc:Ignorable="d" + d:DesignHeight="60" + d:DesignWidth="600"> + + + - - - + + + - - - + + - - - + + - - public partial class HotkeyItem : UserControl { + private static readonly SolidColorBrush HotkeyValueForeground = CreateFrozenBrush(0xFA, 0xFA, 0xFA); + private static readonly SolidColorBrush HotkeyPlaceholderForeground = CreateFrozenBrush(0xA1, 0xA1, 0xAA); + + private static SolidColorBrush CreateFrozenBrush(byte r, byte g, byte b) + { + var brush = new SolidColorBrush(Color.FromRgb(r, g, b)); + brush.Freeze(); + return brush; + } + #region Events /// /// 快捷键变更事件 @@ -80,13 +90,13 @@ namespace Ink_Canvas.Windows if (_currentKey == Key.None) { CurrentHotkeyTextBlock.Text = "未设置"; - CurrentHotkeyTextBlock.Foreground = Brushes.Gray; + CurrentHotkeyTextBlock.Foreground = HotkeyPlaceholderForeground; } else { var modifiersText = _currentModifiers == ModifierKeys.None ? "" : $"{_currentModifiers}+"; CurrentHotkeyTextBlock.Text = $"{modifiersText}{_currentKey}"; - CurrentHotkeyTextBlock.Foreground = Brushes.Black; + CurrentHotkeyTextBlock.Foreground = HotkeyValueForeground; } } @@ -106,7 +116,7 @@ namespace Ink_Canvas.Windows private void StopHotkeyCapture() { BtnSetHotkey.Content = "设置"; - BtnSetHotkey.Background = Brushes.DodgerBlue; + BtnSetHotkey.ClearValue(Button.BackgroundProperty); // 移除键盘事件处理器 KeyDown -= HotkeyItem_KeyDown; diff --git a/Ink Canvas/Windows/HotkeySettingsWindow.xaml b/Ink Canvas/Windows/HotkeySettingsWindow.xaml index 60928011..18d6518e 100644 --- a/Ink Canvas/Windows/HotkeySettingsWindow.xaml +++ b/Ink Canvas/Windows/HotkeySettingsWindow.xaml @@ -6,211 +6,370 @@ xmlns:local="clr-namespace:Ink_Canvas.Windows" xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern" xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf" - ui:ThemeManager.RequestedTheme="Light" - Background="#F9F9F9" - AllowsTransparency="True" - mc:Ignorable="d" - WindowStyle="None" + mc:Ignorable="d" + Title="快捷键设置" + Height="600" + Width="800" WindowStartupLocation="CenterScreen" ResizeMode="CanResize" - Title="快捷键设置" - Height="600" - Width="800"> - + FontFamily="Microsoft YaHei UI" + ui:ThemeManager.IsThemeAware="True" + ui:TitleBar.ExtendViewIntoTitleBar="True" + ui:WindowHelper.SystemBackdropType="Mica" + ui:WindowHelper.UseModernWindowStyle="True" + ui:TitleBar.Height="48"> + + + + + + + + + + + + + + + + - - - + + + - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -