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