add:浮动栏自定义

This commit is contained in:
2026-05-01 17:20:47 +08:00
parent 5fc92cdd10
commit 6980abe331
27 changed files with 690 additions and 20 deletions
@@ -0,0 +1,18 @@
using System.Windows;
namespace Ink_Canvas.Controls.Toolbar
{
/// <summary>
/// 工具栏按钮插件与宿主之间的桥梁。Phase 1 粗粒度暴露 MainWindow,后续收窄。
/// </summary>
public interface IToolbarHost
{
MainWindow Window { get; }
/// <summary>按 id 登记按钮的 view 实例(供 MainWindow 字段回填和互相查找)。</summary>
void RegisterView(string id, FrameworkElement view);
/// <summary>按 id 获取之前注册的 view。不存在返回 null。</summary>
FrameworkElement FindView(string id);
}
}
@@ -0,0 +1,29 @@
using System.Windows;
namespace Ink_Canvas.Controls.Toolbar
{
/// <summary>
/// 一个工具栏按钮(或任意浮动栏/白板栏条目)的插件化契约。
/// 实现类必须有无参构造函数,启动时会被 ToolbarRegistry 反射实例化。
/// </summary>
public interface IToolbarItem
{
/// <summary>稳定、唯一的 id,用于持久化用户配置。不要随便改。</summary>
string Id { get; }
ToolbarSlot DefaultSlot { get; }
/// <summary>同一 slot 内的默认顺序,小的在前。</summary>
int DefaultOrder { get; }
bool DefaultVisible { get; }
ToolbarInsertPosition DefaultPosition { get; }
/// <summary>仅当 Position 为 BeforeAnchor/AfterAnchor 时有意义,对应 XAML 里 x:Name。</summary>
string DefaultAnchorName { get; }
/// <summary>构造 UI 元素并接线所有行为。</summary>
FrameworkElement BuildView(IToolbarHost host);
}
}
@@ -0,0 +1,26 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
/// <summary>
/// 清空按钮。位置:夹在颜色面板与 StackPanelCanvasControls 之间,
/// 所以用 BeforeAnchor 锚到 StackPanelCanvasControls。
/// </summary>
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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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" });
}
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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
{
/// <summary>
/// 通用 ToolbarImageButton 工具栏条目基类——大幅减少每个按钮的样板代码。
/// 派生类通常只需给 Id / 本地化键 / Slot / Order / 点击处理 / Attach 回填。
/// </summary>
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;
/// <summary>DynamicResource 名称,用于 IconBrush。默认为 null(使用控件自带前景色)。</summary>
protected virtual string IconBrushResourceKey => null;
protected abstract void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e);
/// <summary>构建后调用,用于回填 MainWindow 的原命名属性(partial 扩展里的 Attach*)。可选。</summary>
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;
}
}
}
@@ -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);
}
}
@@ -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" });
}
}
}
@@ -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);
}
}
@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Windows;
namespace Ink_Canvas.Controls.Toolbar
{
/// <summary>
/// MainWindow 版的 IToolbarHost 实现。Phase 1 直接把 MainWindow 引用暴露给插件,
/// 插件可通过 host.Window 访问私有/内部成员(partial class 扩展或 internal 字段)。
/// 后续阶段逐步把具体行为抽成 Host 上的方法/事件,收窄这个接口。
/// </summary>
public sealed class ToolbarHost : IToolbarHost
{
private readonly Dictionary<string, FrameworkElement> _views = new Dictionary<string, FrameworkElement>();
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;
}
}
}
@@ -0,0 +1,14 @@
namespace Ink_Canvas.Controls.Toolbar
{
public enum ToolbarInsertPosition
{
/// <summary>从容器头部依次插入;Order 小的在前。</summary>
Prepend,
/// <summary>追加到容器末尾。</summary>
Append,
/// <summary>插入到由 AnchorName 指定的已有元素之前。</summary>
BeforeAnchor,
/// <summary>插入到由 AnchorName 指定的已有元素之后(同一锚点多项按 Order 依次排列)。</summary>
AfterAnchor
}
}
@@ -0,0 +1,33 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace Ink_Canvas.Controls.Toolbar
{
/// <summary>
/// 单个工具栏按钮的用户配置(可见性、顺序、所属 slot、插入位置)。
/// 由 Settings.Toolbar 持久化。
/// </summary>
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<string, ToolbarItemConfig> Items { get; set; } = new Dictionary<string, ToolbarItemConfig>();
}
}
@@ -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
{
/// <summary>
/// 扫描当前程序集里的 IToolbarItem 实现,按用户配置(Settings.Toolbar)排序/过滤后注入到目标容器。
/// </summary>
public static class ToolbarRegistry
{
private static List<IToolbarItem> _items;
public static IReadOnlyList<IToolbarItem> 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;
}
/// <summary>按 slot 分配工具栏条目到对应容器。调用者负责清空目标容器里要被接管的旧内容。</summary>
public static void Populate(IToolbarHost host, IDictionary<ToolbarSlot, Panel> slots, ToolbarLayoutSettings layout)
{
if (host == null || slots == null) return;
layout = layout ?? new ToolbarLayoutSettings();
var grouped = new Dictionary<ToolbarSlot, List<(IToolbarItem item, ToolbarItemConfig cfg)>>();
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;
}
}
}
}
@@ -0,0 +1,11 @@
namespace Ink_Canvas.Controls.Toolbar
{
public enum ToolbarSlot
{
FloatingBarMain,
FloatingBarCanvasControls,
FloatingBarEnd,
BlackboardLeft,
BlackboardRight
}
}