feat(工具栏设置): 添加工具栏配置页面及功能实现

实现工具栏配置页面,允许用户调整工具栏按钮的顺序和可见性
包含主工具栏、画布控制和尾部按钮三个区域的配置
支持恢复默认布局功能
This commit is contained in:
PrefacedCorg
2026-05-02 13:41:02 +08:00
parent 7736d88657
commit 723c0b9cdc
18 changed files with 714 additions and 206 deletions
+4 -8
View File
@@ -2,28 +2,24 @@ 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>
string DisplayName { get; }
string MenuPanelName { get; }
FrameworkElement BuildView(IToolbarHost host);
}
}
@@ -8,6 +8,7 @@ namespace Ink_Canvas.Controls.Toolbar.Items
public override string LocalizationKey => "FloatingBar_AreaEraser";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
public override int DefaultOrder => 100;
public override string MenuPanelName => "EraserSizePanel";
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.EraserIcon_Click(sender, e);
@@ -8,6 +8,7 @@ namespace Ink_Canvas.Controls.Toolbar.Items
public override string LocalizationKey => "FloatingBar_Annotate";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain;
public override int DefaultOrder => 110;
public override string MenuPanelName => "PenPalette";
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.PenIcon_Click(sender, e);
@@ -8,6 +8,7 @@ namespace Ink_Canvas.Controls.Toolbar.Items
public override string LocalizationKey => "FloatingBar_Geometry";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
public override int DefaultOrder => 130;
public override string MenuPanelName => "BorderDrawShape";
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.ImageDrawShape_MouseUp(sender, e);
@@ -5,10 +5,6 @@ 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; }
@@ -19,22 +15,22 @@ namespace Ink_Canvas.Controls.Toolbar.Items
public virtual ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Prepend;
public virtual string DefaultAnchorName => null;
/// <summary>DynamicResource 名称,用于 IconBrush。默认为 null(使用控件自带前景色)。</summary>
protected virtual string IconBrushResourceKey => null;
public string DisplayName => Strings.GetString(LocalizationKey) ?? LocalizationKey;
public virtual string MenuPanelName => null;
/// <summary>DynamicResource 名称,用于 LabelBrush(文字颜色)。默认为 null(使用控件自带前景色)。</summary>
protected virtual string IconBrushResourceKey => null;
protected virtual string LabelBrushResourceKey => 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
Label = Strings.GetString(LocalizationKey) ?? LocalizationKey,
Tag = "ToolbarRegistryInjected"
};
if (!string.IsNullOrEmpty(IconBrushResourceKey))
{
@@ -10,6 +10,7 @@ namespace Ink_Canvas.Controls.Toolbar.Items
public override int DefaultOrder => 110;
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor;
public override string DefaultAnchorName => "FloatingBarEndSeparator";
public override string MenuPanelName => "BorderTools";
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.SymbolIconTools_MouseUp(sender, e);
+51 -8
View File
@@ -8,12 +8,10 @@ using System.Windows.Controls;
namespace Ink_Canvas.Controls.Toolbar
{
/// <summary>
/// 扫描当前程序集里的 IToolbarItem 实现,按用户配置(Settings.Toolbar)排序/过滤后注入到目标容器。
/// </summary>
public static class ToolbarRegistry
{
private static List<IToolbarItem> _items;
internal const string InjectedTag = "ToolbarRegistryInjected";
public static IReadOnlyList<IToolbarItem> Discover()
{
@@ -34,13 +32,25 @@ namespace Ink_Canvas.Controls.Toolbar
})
.Where(i => i != null)
.ToList();
LogHelper.WriteLogToFile($"ToolbarRegistry: Discover 完成, 发现 {_items.Count} 个条目", LogHelper.LogType.Info);
return _items;
}
/// <summary>按 slot 分配工具栏条目到对应容器。调用者负责清空目标容器里要被接管的旧内容。</summary>
public static void ClearInjected(Panel container)
{
if (container == null) return;
var toRemove = container.Children.OfType<FrameworkElement>()
.Where(e => e.Tag as string == InjectedTag)
.ToList();
foreach (var element in toRemove)
container.Children.Remove(element);
LogHelper.WriteLogToFile($"ToolbarRegistry: ClearInjected 清除 {toRemove.Count} 个元素 [{container.Name}]", LogHelper.LogType.Info);
}
public static void Populate(IToolbarHost host, IDictionary<ToolbarSlot, Panel> slots, ToolbarLayoutSettings layout)
{
if (host == null || slots == null) return;
LogHelper.WriteLogToFile($"ToolbarRegistry: Populate 开始", LogHelper.LogType.Info);
if (host == null || slots == null) { LogHelper.WriteLogToFile("ToolbarRegistry: Populate host 或 slots 为空", LogHelper.LogType.Warning); return; }
layout = layout ?? new ToolbarLayoutSettings();
var grouped = new Dictionary<ToolbarSlot, List<(IToolbarItem item, ToolbarItemConfig cfg)>>();
@@ -65,18 +75,51 @@ namespace Ink_Canvas.Controls.Toolbar
}
list.Add((item, cfg));
}
LogHelper.WriteLogToFile($"ToolbarRegistry: 分组完成, {grouped.Count} 个 slot 有可见条目", LogHelper.LogType.Info);
foreach (var kv in grouped)
{
if (!slots.TryGetValue(kv.Key, out var container) || container == null) continue;
LogHelper.WriteLogToFile($"ToolbarRegistry: 注入到 {kv.Key}, 条目数={kv.Value.Count}", LogHelper.LogType.Info);
InjectIntoContainer(host, container, kv.Value);
}
ApplyMenuVisibility(host, layout);
LogHelper.WriteLogToFile($"ToolbarRegistry: Populate 完成", LogHelper.LogType.Info);
}
public static void ApplyMenuVisibility(IToolbarHost host, ToolbarLayoutSettings layout)
{
if (host == null || layout == null) return;
foreach (var item in Discover())
{
if (string.IsNullOrEmpty(item.MenuPanelName)) continue;
bool visible = true;
if (layout.Items.TryGetValue(item.Id, out var cfg))
visible = cfg.Visible;
try
{
var menuElement = host.Window.FindName(item.MenuPanelName) as FrameworkElement;
if (menuElement != null)
{
menuElement.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
LogHelper.WriteLogToFile($"ToolbarRegistry: 菜单 [{item.MenuPanelName}] -> {(visible ? "Visible" : "Collapsed")}", LogHelper.LogType.Info);
}
else
{
LogHelper.WriteLogToFile($"ToolbarRegistry: 找不到菜单面板 [{item.MenuPanelName}]", LogHelper.LogType.Warning);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"ToolbarRegistry: 设置菜单可见性异常 [{item.MenuPanelName}]: {ex.Message}", LogHelper.LogType.Warning);
}
}
}
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();
@@ -153,9 +196,9 @@ namespace Ink_Canvas.Controls.Toolbar
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"ToolbarRegistry: 构建 {item.Id} 失败: {ex.Message}", LogHelper.LogType.Warning);
LogHelper.WriteLogToFile($"ToolbarRegistry: 构建 {item.Id} 失败: {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}", LogHelper.LogType.Error);
return null;
}
}
}
}
}