From 6980abe33163af9e3e514578faaa158e9ee20f7a Mon Sep 17 00:00:00 2001 From: CJKmkp <2564608840@qq.com> Date: Fri, 1 May 2026 17:20:47 +0800 Subject: [PATCH] =?UTF-8?q?add:=E6=B5=AE=E5=8A=A8=E6=A0=8F=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/Controls/Toolbar/IToolbarHost.cs | 18 ++ Ink Canvas/Controls/Toolbar/IToolbarItem.cs | 29 ++++ .../Controls/Toolbar/Items/ClearToolItem.cs | 26 +++ .../Controls/Toolbar/Items/CursorToolItem.cs | 18 ++ .../Toolbar/Items/CursorWithDelToolItem.cs | 19 +++ .../Toolbar/Items/EraserByStrokesToolItem.cs | 18 ++ .../Controls/Toolbar/Items/EraserToolItem.cs | 18 ++ .../Controls/Toolbar/Items/FoldToolItem.cs | 20 +++ .../Controls/Toolbar/Items/PenToolItem.cs | 18 ++ .../Controls/Toolbar/Items/RedoToolItem.cs | 23 +++ .../Controls/Toolbar/Items/SelectToolItem.cs | 18 ++ .../Toolbar/Items/ShapeDrawToolItem.cs | 18 ++ .../Items/ToolbarImageButtonItemBase.cs | 47 +++++ .../Controls/Toolbar/Items/ToolsToolItem.cs | 20 +++ .../Controls/Toolbar/Items/UndoToolItem.cs | 23 +++ .../Toolbar/Items/WhiteboardToolItem.cs | 20 +++ Ink Canvas/Controls/Toolbar/ToolbarHost.cs | 34 ++++ .../Controls/Toolbar/ToolbarInsertPosition.cs | 14 ++ .../Controls/Toolbar/ToolbarItemConfig.cs | 33 ++++ .../Controls/Toolbar/ToolbarRegistry.cs | 161 ++++++++++++++++++ Ink Canvas/Controls/Toolbar/ToolbarSlot.cs | 11 ++ Ink Canvas/MainWindow.xaml | 23 +-- Ink Canvas/MainWindow.xaml.cs | 13 ++ .../MainWindow_cs/MW_FloatingBarIcons.cs | 6 +- Ink Canvas/MainWindow_cs/MW_ShapeDrawing.cs | 2 +- Ink Canvas/MainWindow_cs/MW_Toolbar.cs | 56 ++++++ Ink Canvas/Resources/Settings.cs | 4 + 27 files changed, 690 insertions(+), 20 deletions(-) create mode 100644 Ink Canvas/Controls/Toolbar/IToolbarHost.cs create mode 100644 Ink Canvas/Controls/Toolbar/IToolbarItem.cs create mode 100644 Ink Canvas/Controls/Toolbar/Items/ClearToolItem.cs create mode 100644 Ink Canvas/Controls/Toolbar/Items/CursorToolItem.cs create mode 100644 Ink Canvas/Controls/Toolbar/Items/CursorWithDelToolItem.cs create mode 100644 Ink Canvas/Controls/Toolbar/Items/EraserByStrokesToolItem.cs create mode 100644 Ink Canvas/Controls/Toolbar/Items/EraserToolItem.cs create mode 100644 Ink Canvas/Controls/Toolbar/Items/FoldToolItem.cs create mode 100644 Ink Canvas/Controls/Toolbar/Items/PenToolItem.cs create mode 100644 Ink Canvas/Controls/Toolbar/Items/RedoToolItem.cs create mode 100644 Ink Canvas/Controls/Toolbar/Items/SelectToolItem.cs create mode 100644 Ink Canvas/Controls/Toolbar/Items/ShapeDrawToolItem.cs create mode 100644 Ink Canvas/Controls/Toolbar/Items/ToolbarImageButtonItemBase.cs create mode 100644 Ink Canvas/Controls/Toolbar/Items/ToolsToolItem.cs create mode 100644 Ink Canvas/Controls/Toolbar/Items/UndoToolItem.cs create mode 100644 Ink Canvas/Controls/Toolbar/Items/WhiteboardToolItem.cs create mode 100644 Ink Canvas/Controls/Toolbar/ToolbarHost.cs create mode 100644 Ink Canvas/Controls/Toolbar/ToolbarInsertPosition.cs create mode 100644 Ink Canvas/Controls/Toolbar/ToolbarItemConfig.cs create mode 100644 Ink Canvas/Controls/Toolbar/ToolbarRegistry.cs create mode 100644 Ink Canvas/Controls/Toolbar/ToolbarSlot.cs create mode 100644 Ink Canvas/MainWindow_cs/MW_Toolbar.cs diff --git a/Ink Canvas/Controls/Toolbar/IToolbarHost.cs b/Ink Canvas/Controls/Toolbar/IToolbarHost.cs new file mode 100644 index 00000000..8d4106f4 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/IToolbarHost.cs @@ -0,0 +1,18 @@ +using System.Windows; + +namespace Ink_Canvas.Controls.Toolbar +{ + /// + /// 工具栏按钮插件与宿主之间的桥梁。Phase 1 粗粒度暴露 MainWindow,后续收窄。 + /// + public interface IToolbarHost + { + MainWindow Window { get; } + + /// 按 id 登记按钮的 view 实例(供 MainWindow 字段回填和互相查找)。 + void RegisterView(string id, FrameworkElement view); + + /// 按 id 获取之前注册的 view。不存在返回 null。 + FrameworkElement FindView(string id); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/IToolbarItem.cs b/Ink Canvas/Controls/Toolbar/IToolbarItem.cs new file mode 100644 index 00000000..0c7498c7 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/IToolbarItem.cs @@ -0,0 +1,29 @@ +using System.Windows; + +namespace Ink_Canvas.Controls.Toolbar +{ + /// + /// 一个工具栏按钮(或任意浮动栏/白板栏条目)的插件化契约。 + /// 实现类必须有无参构造函数,启动时会被 ToolbarRegistry 反射实例化。 + /// + public interface IToolbarItem + { + /// 稳定、唯一的 id,用于持久化用户配置。不要随便改。 + string Id { get; } + + ToolbarSlot DefaultSlot { get; } + + /// 同一 slot 内的默认顺序,小的在前。 + int DefaultOrder { get; } + + bool DefaultVisible { get; } + + ToolbarInsertPosition DefaultPosition { get; } + + /// 仅当 Position 为 BeforeAnchor/AfterAnchor 时有意义,对应 XAML 里 x:Name。 + string DefaultAnchorName { get; } + + /// 构造 UI 元素并接线所有行为。 + FrameworkElement BuildView(IToolbarHost host); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/ClearToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/ClearToolItem.cs new file mode 100644 index 00000000..330cab1a --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/ClearToolItem.cs @@ -0,0 +1,26 @@ +using System.Windows.Input; + +namespace Ink_Canvas.Controls.Toolbar.Items +{ + /// + /// 清空按钮。位置:夹在颜色面板与 StackPanelCanvasControls 之间, + /// 所以用 BeforeAnchor 锚到 StackPanelCanvasControls。 + /// + internal sealed class ClearToolItem : ToolbarImageButtonItemBase + { + public override string Id => "builtin.clear"; + public override string LocalizationKey => "FloatingBar_Clear"; + public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain; + public override int DefaultOrder => 0; + public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.BeforeAnchor; + public override string DefaultAnchorName => "StackPanelCanvasControls"; + + protected override string IconBrushResourceKey => "RedBrush"; + + protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) + => host.Window.SymbolIconDelete_MouseUp(sender, e); + + protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view) + => host.Window.AttachSymbolIconDelete(view); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/CursorToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/CursorToolItem.cs new file mode 100644 index 00000000..e86ba866 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/CursorToolItem.cs @@ -0,0 +1,18 @@ +using System.Windows.Input; + +namespace Ink_Canvas.Controls.Toolbar.Items +{ + internal sealed class CursorToolItem : ToolbarImageButtonItemBase + { + public override string Id => "builtin.cursor"; + public override string LocalizationKey => "FloatingBar_Mouse"; + public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain; + public override int DefaultOrder => 100; + + protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) + => host.Window.CursorIcon_Click(sender, e); + + protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view) + => host.Window.AttachCursorIconView(view); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/CursorWithDelToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/CursorWithDelToolItem.cs new file mode 100644 index 00000000..0234b7a5 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/CursorWithDelToolItem.cs @@ -0,0 +1,19 @@ +using System.Windows.Input; + +namespace Ink_Canvas.Controls.Toolbar.Items +{ + internal sealed class CursorWithDelToolItem : ToolbarImageButtonItemBase + { + public override string Id => "builtin.cursorWithDel"; + public override string LocalizationKey => "FloatingBar_ClearAndMouse"; + public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls; + public override int DefaultOrder => 320; + public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Append; + + protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) + => host.Window.CursorWithDelIcon_Click(sender, e); + + protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view) + => host.Window.AttachCursorWithDelBtn(view); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/EraserByStrokesToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/EraserByStrokesToolItem.cs new file mode 100644 index 00000000..752328c8 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/EraserByStrokesToolItem.cs @@ -0,0 +1,18 @@ +using System.Windows.Input; + +namespace Ink_Canvas.Controls.Toolbar.Items +{ + internal sealed class EraserByStrokesToolItem : ToolbarImageButtonItemBase + { + public override string Id => "builtin.eraserByStrokes"; + public override string LocalizationKey => "FloatingBar_StrokeEraser"; + public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls; + public override int DefaultOrder => 110; + + protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) + => host.Window.EraserIconByStrokes_Click(sender, e); + + protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view) + => host.Window.AttachEraserByStrokesIcon(view); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/EraserToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/EraserToolItem.cs new file mode 100644 index 00000000..fb44594a --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/EraserToolItem.cs @@ -0,0 +1,18 @@ +using System.Windows.Input; + +namespace Ink_Canvas.Controls.Toolbar.Items +{ + internal sealed class EraserToolItem : ToolbarImageButtonItemBase + { + public override string Id => "builtin.eraser"; + public override string LocalizationKey => "FloatingBar_AreaEraser"; + public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls; + public override int DefaultOrder => 100; + + protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) + => host.Window.EraserIcon_Click(sender, e); + + protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view) + => host.Window.AttachEraserIcon(view); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/FoldToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/FoldToolItem.cs new file mode 100644 index 00000000..dcb7942b --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/FoldToolItem.cs @@ -0,0 +1,20 @@ +using System.Windows.Input; + +namespace Ink_Canvas.Controls.Toolbar.Items +{ + internal sealed class FoldToolItem : ToolbarImageButtonItemBase + { + public override string Id => "builtin.fold"; + public override string LocalizationKey => "FloatingBar_Hide"; + public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarEnd; + public override int DefaultOrder => 120; + public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor; + public override string DefaultAnchorName => "FloatingBarEndSeparator"; + + protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) + => host.Window.FoldFloatingBar_MouseUp(sender, e); + + protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view) + => host.Window.AttachFoldIcon(view); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/PenToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/PenToolItem.cs new file mode 100644 index 00000000..8f339eb0 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/PenToolItem.cs @@ -0,0 +1,18 @@ +using System.Windows.Input; + +namespace Ink_Canvas.Controls.Toolbar.Items +{ + internal sealed class PenToolItem : ToolbarImageButtonItemBase + { + public override string Id => "builtin.pen"; + public override string LocalizationKey => "FloatingBar_Annotate"; + public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain; + public override int DefaultOrder => 110; + + protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) + => host.Window.PenIcon_Click(sender, e); + + protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view) + => host.Window.AttachPenIconView(view); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/RedoToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/RedoToolItem.cs new file mode 100644 index 00000000..76da523b --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/RedoToolItem.cs @@ -0,0 +1,23 @@ +using System.Windows.Input; + +namespace Ink_Canvas.Controls.Toolbar.Items +{ + internal sealed class RedoToolItem : ToolbarImageButtonItemBase + { + public override string Id => "builtin.redo"; + public override string LocalizationKey => "Board_Redo"; + public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls; + public override int DefaultOrder => 310; + public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Append; + + protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) + => host.Window.SymbolIconRedo_MouseUp(sender, e); + + protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view) + { + host.Window.AttachSymbolIconRedo(view); + view.SetBinding(System.Windows.UIElement.IsEnabledProperty, + new System.Windows.Data.Binding("IsEnabled") { ElementName = "BtnRedo" }); + } + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/SelectToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/SelectToolItem.cs new file mode 100644 index 00000000..671a6c75 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/SelectToolItem.cs @@ -0,0 +1,18 @@ +using System.Windows.Input; + +namespace Ink_Canvas.Controls.Toolbar.Items +{ + internal sealed class SelectToolItem : ToolbarImageButtonItemBase + { + public override string Id => "builtin.select"; + public override string LocalizationKey => "FloatingBar_LassoSelect"; + public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls; + public override int DefaultOrder => 120; + + protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) + => host.Window.SymbolIconSelect_MouseUp(sender, e); + + protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view) + => host.Window.AttachSymbolIconSelect(view); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/ShapeDrawToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/ShapeDrawToolItem.cs new file mode 100644 index 00000000..675cc43a --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/ShapeDrawToolItem.cs @@ -0,0 +1,18 @@ +using System.Windows.Input; + +namespace Ink_Canvas.Controls.Toolbar.Items +{ + internal sealed class ShapeDrawToolItem : ToolbarImageButtonItemBase + { + public override string Id => "builtin.shapeDraw"; + public override string LocalizationKey => "FloatingBar_Geometry"; + public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls; + public override int DefaultOrder => 130; + + protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) + => host.Window.ImageDrawShape_MouseUp(sender, e); + + protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view) + => host.Window.AttachShapeDrawBtn(view); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/ToolbarImageButtonItemBase.cs b/Ink Canvas/Controls/Toolbar/Items/ToolbarImageButtonItemBase.cs new file mode 100644 index 00000000..4e174778 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/ToolbarImageButtonItemBase.cs @@ -0,0 +1,47 @@ +using Ink_Canvas.Properties; +using System; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; + +namespace Ink_Canvas.Controls.Toolbar.Items +{ + /// + /// 通用 ToolbarImageButton 工具栏条目基类——大幅减少每个按钮的样板代码。 + /// 派生类通常只需给 Id / 本地化键 / Slot / Order / 点击处理 / Attach 回填。 + /// + internal abstract class ToolbarImageButtonItemBase : IToolbarItem + { + public abstract string Id { get; } + public abstract string LocalizationKey { get; } + public abstract ToolbarSlot DefaultSlot { get; } + public abstract int DefaultOrder { get; } + public virtual bool DefaultVisible => true; + public virtual ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Prepend; + public virtual string DefaultAnchorName => null; + + /// DynamicResource 名称,用于 IconBrush。默认为 null(使用控件自带前景色)。 + protected virtual string IconBrushResourceKey => null; + + protected abstract void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e); + + /// 构建后调用,用于回填 MainWindow 的原命名属性(partial 扩展里的 Attach*)。可选。 + protected virtual void AfterBuild(IToolbarHost host, ToolbarImageButton view) { } + + public FrameworkElement BuildView(IToolbarHost host) + { + var btn = new ToolbarImageButton + { + Label = Strings.GetString(LocalizationKey) ?? LocalizationKey + }; + if (!string.IsNullOrEmpty(IconBrushResourceKey)) + { + if (btn.TryFindResource(IconBrushResourceKey) is Brush brush) btn.IconBrush = brush; + else btn.SetResourceReference(ToolbarImageButton.IconBrushProperty, IconBrushResourceKey); + } + btn.ButtonMouseUp += (s, e) => OnClick(host, s, e); + AfterBuild(host, btn); + return btn; + } + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/ToolsToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/ToolsToolItem.cs new file mode 100644 index 00000000..cb80b8f8 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/ToolsToolItem.cs @@ -0,0 +1,20 @@ +using System.Windows.Input; + +namespace Ink_Canvas.Controls.Toolbar.Items +{ + internal sealed class ToolsToolItem : ToolbarImageButtonItemBase + { + public override string Id => "builtin.tools"; + public override string LocalizationKey => "Board_Tools"; + public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarEnd; + public override int DefaultOrder => 110; + public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor; + public override string DefaultAnchorName => "FloatingBarEndSeparator"; + + protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) + => host.Window.SymbolIconTools_MouseUp(sender, e); + + protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view) + => host.Window.AttachToolsBtn(view); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/UndoToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/UndoToolItem.cs new file mode 100644 index 00000000..7d006d83 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/UndoToolItem.cs @@ -0,0 +1,23 @@ +using System.Windows.Input; + +namespace Ink_Canvas.Controls.Toolbar.Items +{ + internal sealed class UndoToolItem : ToolbarImageButtonItemBase + { + public override string Id => "builtin.undo"; + public override string LocalizationKey => "Board_Undo"; + public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls; + public override int DefaultOrder => 300; + public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Append; + + protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) + => host.Window.SymbolIconUndo_MouseUp(sender, e); + + protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view) + { + host.Window.AttachSymbolIconUndo(view); + view.SetBinding(System.Windows.UIElement.IsEnabledProperty, + new System.Windows.Data.Binding("IsEnabled") { ElementName = "BtnUndo" }); + } + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/WhiteboardToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/WhiteboardToolItem.cs new file mode 100644 index 00000000..ffe50e68 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/WhiteboardToolItem.cs @@ -0,0 +1,20 @@ +using System.Windows.Input; + +namespace Ink_Canvas.Controls.Toolbar.Items +{ + internal sealed class WhiteboardToolItem : ToolbarImageButtonItemBase + { + public override string Id => "builtin.whiteboard"; + public override string LocalizationKey => "FloatingBar_Whiteboard"; + public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarEnd; + public override int DefaultOrder => 100; + public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor; + public override string DefaultAnchorName => "FloatingBarEndSeparator"; + + protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e) + => host.Window.ImageBlackboard_MouseUp(sender, e); + + protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view) + => host.Window.AttachWhiteboardBtn(view); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/ToolbarHost.cs b/Ink Canvas/Controls/Toolbar/ToolbarHost.cs new file mode 100644 index 00000000..0418a941 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/ToolbarHost.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Windows; + +namespace Ink_Canvas.Controls.Toolbar +{ + /// + /// MainWindow 版的 IToolbarHost 实现。Phase 1 直接把 MainWindow 引用暴露给插件, + /// 插件可通过 host.Window 访问私有/内部成员(partial class 扩展或 internal 字段)。 + /// 后续阶段逐步把具体行为抽成 Host 上的方法/事件,收窄这个接口。 + /// + public sealed class ToolbarHost : IToolbarHost + { + private readonly Dictionary _views = new Dictionary(); + + public ToolbarHost(MainWindow window) + { + Window = window; + } + + public MainWindow Window { get; } + + public void RegisterView(string id, FrameworkElement view) + { + if (string.IsNullOrEmpty(id) || view == null) return; + _views[id] = view; + } + + public FrameworkElement FindView(string id) + { + if (string.IsNullOrEmpty(id)) return null; + return _views.TryGetValue(id, out var v) ? v : null; + } + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/ToolbarInsertPosition.cs b/Ink Canvas/Controls/Toolbar/ToolbarInsertPosition.cs new file mode 100644 index 00000000..4b1159ea --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/ToolbarInsertPosition.cs @@ -0,0 +1,14 @@ +namespace Ink_Canvas.Controls.Toolbar +{ + public enum ToolbarInsertPosition + { + /// 从容器头部依次插入;Order 小的在前。 + Prepend, + /// 追加到容器末尾。 + Append, + /// 插入到由 AnchorName 指定的已有元素之前。 + BeforeAnchor, + /// 插入到由 AnchorName 指定的已有元素之后(同一锚点多项按 Order 依次排列)。 + AfterAnchor + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/ToolbarItemConfig.cs b/Ink Canvas/Controls/Toolbar/ToolbarItemConfig.cs new file mode 100644 index 00000000..7a364e42 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/ToolbarItemConfig.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Ink_Canvas.Controls.Toolbar +{ + /// + /// 单个工具栏按钮的用户配置(可见性、顺序、所属 slot、插入位置)。 + /// 由 Settings.Toolbar 持久化。 + /// + public class ToolbarItemConfig + { + [JsonProperty("visible")] + public bool Visible { get; set; } = true; + + [JsonProperty("order")] + public int Order { get; set; } + + [JsonProperty("slot")] + public ToolbarSlot Slot { get; set; } = ToolbarSlot.FloatingBarMain; + + [JsonProperty("position")] + public ToolbarInsertPosition Position { get; set; } = ToolbarInsertPosition.Prepend; + + [JsonProperty("anchorName")] + public string AnchorName { get; set; } + } + + public class ToolbarLayoutSettings + { + [JsonProperty("items")] + public Dictionary Items { get; set; } = new Dictionary(); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/ToolbarRegistry.cs b/Ink Canvas/Controls/Toolbar/ToolbarRegistry.cs new file mode 100644 index 00000000..d8ea1dee --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/ToolbarRegistry.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Windows; +using System.Windows.Controls; +using Ink_Canvas.Helpers; + +namespace Ink_Canvas.Controls.Toolbar +{ + /// + /// 扫描当前程序集里的 IToolbarItem 实现,按用户配置(Settings.Toolbar)排序/过滤后注入到目标容器。 + /// + public static class ToolbarRegistry + { + private static List _items; + + public static IReadOnlyList Discover() + { + if (_items != null) return _items; + + var itemType = typeof(IToolbarItem); + _items = Assembly.GetExecutingAssembly() + .GetTypes() + .Where(t => !t.IsAbstract && !t.IsInterface && itemType.IsAssignableFrom(t)) + .Select(t => + { + try { return (IToolbarItem)Activator.CreateInstance(t); } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"ToolbarRegistry: 实例化 {t.FullName} 失败: {ex.Message}", LogHelper.LogType.Warning); + return null; + } + }) + .Where(i => i != null) + .ToList(); + return _items; + } + + /// 按 slot 分配工具栏条目到对应容器。调用者负责清空目标容器里要被接管的旧内容。 + public static void Populate(IToolbarHost host, IDictionary slots, ToolbarLayoutSettings layout) + { + if (host == null || slots == null) return; + layout = layout ?? new ToolbarLayoutSettings(); + + var grouped = new Dictionary>(); + foreach (var item in Discover()) + { + if (!layout.Items.TryGetValue(item.Id, out var cfg)) + { + cfg = new ToolbarItemConfig + { + Visible = item.DefaultVisible, + Order = item.DefaultOrder, + Slot = item.DefaultSlot, + Position = item.DefaultPosition, + AnchorName = item.DefaultAnchorName + }; + } + if (!cfg.Visible) continue; + if (!grouped.TryGetValue(cfg.Slot, out var list)) + { + list = new List<(IToolbarItem, ToolbarItemConfig)>(); + grouped[cfg.Slot] = list; + } + list.Add((item, cfg)); + } + + foreach (var kv in grouped) + { + if (!slots.TryGetValue(kv.Key, out var container) || container == null) continue; + InjectIntoContainer(host, container, kv.Value); + } + } + + private static void InjectIntoContainer(IToolbarHost host, Panel container, + List<(IToolbarItem item, ToolbarItemConfig cfg)> entries) + { + // 按 Position 分桶,每桶内按 Order 升序。 + var prepend = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.Prepend).OrderBy(e => e.cfg.Order).ToList(); + var append = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.Append).OrderBy(e => e.cfg.Order).ToList(); + var before = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.BeforeAnchor).ToList(); + var after = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.AfterAnchor).ToList(); + + var prependIndex = 0; + foreach (var entry in prepend) + { + var view = BuildAndRegister(host, entry.item); + if (view == null) continue; + container.Children.Insert(prependIndex++, view); + } + + foreach (var entry in append) + { + var view = BuildAndRegister(host, entry.item); + if (view == null) continue; + container.Children.Add(view); + } + + foreach (var group in before.GroupBy(e => e.cfg.AnchorName)) + { + var anchor = FindNamedChild(container, group.Key); + if (anchor == null) + { + LogHelper.WriteLogToFile($"ToolbarRegistry: 未找到锚点 '{group.Key}' (BeforeAnchor)", LogHelper.LogType.Warning); + continue; + } + var idx = container.Children.IndexOf(anchor); + foreach (var entry in group.OrderBy(e => e.cfg.Order)) + { + var view = BuildAndRegister(host, entry.item); + if (view == null) continue; + container.Children.Insert(idx++, view); + } + } + + foreach (var group in after.GroupBy(e => e.cfg.AnchorName)) + { + var anchor = FindNamedChild(container, group.Key); + if (anchor == null) + { + LogHelper.WriteLogToFile($"ToolbarRegistry: 未找到锚点 '{group.Key}' (AfterAnchor)", LogHelper.LogType.Warning); + continue; + } + var idx = container.Children.IndexOf(anchor) + 1; + foreach (var entry in group.OrderBy(e => e.cfg.Order)) + { + var view = BuildAndRegister(host, entry.item); + if (view == null) continue; + container.Children.Insert(idx++, view); + } + } + } + + private static UIElement FindNamedChild(Panel container, string name) + { + if (string.IsNullOrEmpty(name)) return null; + foreach (UIElement child in container.Children) + { + if (child is FrameworkElement fe && fe.Name == name) return child; + } + return null; + } + + private static FrameworkElement BuildAndRegister(IToolbarHost host, IToolbarItem item) + { + try + { + var view = item.BuildView(host); + if (view == null) return null; + host.RegisterView(item.Id, view); + return view; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"ToolbarRegistry: 构建 {item.Id} 失败: {ex.Message}", LogHelper.LogType.Warning); + return null; + } + } + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/ToolbarSlot.cs b/Ink Canvas/Controls/Toolbar/ToolbarSlot.cs new file mode 100644 index 00000000..79ca1e60 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/ToolbarSlot.cs @@ -0,0 +1,11 @@ +namespace Ink_Canvas.Controls.Toolbar +{ + public enum ToolbarSlot + { + FloatingBarMain, + FloatingBarCanvasControls, + FloatingBarEnd, + BlackboardLeft, + BlackboardRight + } +} \ No newline at end of file diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index 22c0d0ea..36576c21 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -2467,9 +2467,7 @@ - - - + @@ -2849,15 +2847,12 @@ ButtonMouseDown="FloatingBarToolBtnMouseDownFeedback_Panel" ButtonMouseLeave="FloatingBarToolBtnMouseLeaveFeedback_Panel" ButtonMouseUp="QuickColorGreen_Click" ToolTip="{i18n:I18n Key=Canvas_Color_Green}"/> - + - - - - + - - - + @@ -3051,12 +3044,10 @@ - - - - - + + @@ -1175,6 +1185,9 @@ namespace Ink_Canvas private void Window_Loaded(object sender, RoutedEventArgs e) { loadPenCanvas(); + // 工具栏插件化按钮先注入到容器,确保 LoadSettings 内部对 Cursor_Icon / Pen_Icon 等的访问非空。 + // Settings.Toolbar 此时尚为默认值(全部可见),与旧 XAML 行为一致。 + InitializeToolbarPlugins(); //加载设置 LoadSettings(true); ApplyLanguageFromSettings(); diff --git a/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs b/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs index 1de2a8f6..2034dab0 100644 --- a/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs +++ b/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs @@ -1685,7 +1685,7 @@ namespace Ink_Canvas /// /// 发送者 /// 鼠标按钮事件参数 - private void SymbolIconTools_MouseUp(object sender, MouseButtonEventArgs e) + internal void SymbolIconTools_MouseUp(object sender, MouseButtonEventArgs e) { if (BorderTools.Visibility == Visibility.Visible || BoardBorderTools.Visibility == Visibility.Visible) { @@ -2793,7 +2793,7 @@ namespace Ink_Canvas /// /// 发送者 /// 路由事件参数 - private void EraserIconByStrokes_Click(object sender, MouseButtonEventArgs e) + internal void EraserIconByStrokes_Click(object sender, MouseButtonEventArgs e) { // 禁用高级橡皮擦系统 DisableEraserOverlay(); @@ -2825,7 +2825,7 @@ namespace Ink_Canvas /// /// 发送者 /// 路由事件参数 - private void CursorWithDelIcon_Click(object sender, MouseButtonEventArgs e) + internal void CursorWithDelIcon_Click(object sender, MouseButtonEventArgs e) { SymbolIconDelete_MouseUp(sender, null); CursorIcon_Click(null, null); diff --git a/Ink Canvas/MainWindow_cs/MW_ShapeDrawing.cs b/Ink Canvas/MainWindow_cs/MW_ShapeDrawing.cs index dc1ec941..c3610b19 100644 --- a/Ink Canvas/MainWindow_cs/MW_ShapeDrawing.cs +++ b/Ink Canvas/MainWindow_cs/MW_ShapeDrawing.cs @@ -32,7 +32,7 @@ namespace Ink_Canvas /// 3. 如果形状绘制面板可见,则隐藏它 /// 4. 如果形状绘制面板不可见,则显示它 /// - private void ImageDrawShape_MouseUp(object sender, MouseButtonEventArgs e) + internal void ImageDrawShape_MouseUp(object sender, MouseButtonEventArgs e) { if (BorderDrawShape.Visibility == Visibility.Visible) { diff --git a/Ink Canvas/MainWindow_cs/MW_Toolbar.cs b/Ink Canvas/MainWindow_cs/MW_Toolbar.cs new file mode 100644 index 00000000..13d81549 --- /dev/null +++ b/Ink Canvas/MainWindow_cs/MW_Toolbar.cs @@ -0,0 +1,56 @@ +using Ink_Canvas.Controls; +using Ink_Canvas.Controls.Toolbar; +using System.Collections.Generic; +using System.Windows.Controls; + +namespace Ink_Canvas +{ + public partial class MainWindow + { + // 这批属性替代了 XAML 中原有的 x:Name 自动生成字段;外部代码继续按原名访问。 + // 由对应 Toolbar Item 的 AfterBuild 回填,Populate 发生在 Window_Loaded 早期。 + internal ToolbarImageButton SymbolIconDelete { get; private set; } + internal ToolbarImageButton Eraser_Icon { get; private set; } + internal ToolbarImageButton EraserByStrokes_Icon { get; private set; } + internal ToolbarImageButton SymbolIconSelect { get; private set; } + internal ToolbarImageButton ShapeDrawFloatingBarBtn { get; private set; } + internal ToolbarImageButton SymbolIconUndo { get; private set; } + internal ToolbarImageButton SymbolIconRedo { get; private set; } + internal ToolbarImageButton CursorWithDelFloatingBarBtn { get; private set; } + internal ToolbarImageButton WhiteboardFloatingBarBtn { get; private set; } + internal ToolbarImageButton ToolsFloatingBarBtn { get; private set; } + internal ToolbarImageButton Fold_Icon { get; private set; } + + internal void AttachCursorIconView(ToolbarImageButton btn) => Cursor_Icon = btn; + internal void AttachPenIconView(ToolbarImageButton btn) => Pen_Icon = btn; + internal void AttachSymbolIconDelete(ToolbarImageButton btn) => SymbolIconDelete = btn; + internal void AttachEraserIcon(ToolbarImageButton btn) => Eraser_Icon = btn; + internal void AttachEraserByStrokesIcon(ToolbarImageButton btn) => EraserByStrokes_Icon = btn; + internal void AttachSymbolIconSelect(ToolbarImageButton btn) => SymbolIconSelect = btn; + internal void AttachShapeDrawBtn(ToolbarImageButton btn) => ShapeDrawFloatingBarBtn = btn; + internal void AttachSymbolIconUndo(ToolbarImageButton btn) => SymbolIconUndo = btn; + internal void AttachSymbolIconRedo(ToolbarImageButton btn) => SymbolIconRedo = btn; + internal void AttachCursorWithDelBtn(ToolbarImageButton btn) => CursorWithDelFloatingBarBtn = btn; + internal void AttachWhiteboardBtn(ToolbarImageButton btn) => WhiteboardFloatingBarBtn = btn; + internal void AttachToolsBtn(ToolbarImageButton btn) => ToolsFloatingBarBtn = btn; + internal void AttachFoldIcon(ToolbarImageButton btn) => Fold_Icon = btn; + + /// + /// 在 Window_Loaded 早期调用:按 Settings.Toolbar 配置把插件化按钮填充到对应容器。 + /// 必须在 LoadSettings 之前,因为 LoadSettings 会访问 Cursor_Icon/Pen_Icon/Eraser_Icon 等。 + /// + internal void InitializeToolbarPlugins() + { + ToolbarHost = new ToolbarHost(this); + var slots = new Dictionary + { + { ToolbarSlot.FloatingBarMain, StackPanelFloatingBar }, + { ToolbarSlot.FloatingBarCanvasControls, StackPanelCanvasControls }, + { ToolbarSlot.FloatingBarEnd, StackPanelFloatingBarEnd }, + { ToolbarSlot.BlackboardLeft, BlackboardLeftSide }, + { ToolbarSlot.BlackboardRight, BlackboardRightSide } + }; + ToolbarRegistry.Populate(ToolbarHost, slots, Settings?.Toolbar); + } + } +} \ No newline at end of file diff --git a/Ink Canvas/Resources/Settings.cs b/Ink Canvas/Resources/Settings.cs index ebdca2de..2c3ebed1 100644 --- a/Ink Canvas/Resources/Settings.cs +++ b/Ink Canvas/Resources/Settings.cs @@ -1,3 +1,4 @@ +using Ink_Canvas.Controls.Toolbar; using Newtonsoft.Json; using System; using System.Collections.Generic; @@ -37,6 +38,9 @@ namespace Ink_Canvas [JsonProperty("security")] public Security Security { get; set; } = new Security(); + + [JsonProperty("toolbar")] + public ToolbarLayoutSettings Toolbar { get; set; } = new ToolbarLayoutSettings(); } public class Security