From 204dcd64987873af8cee625a076a4e41d2bb6237 Mon Sep 17 00:00:00 2001 From: ww-rm Date: Sat, 5 Apr 2025 00:57:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A03.8=E5=8F=8A=E4=BB=A5?= =?UTF-8?q?=E4=B8=8A=E7=89=88=E6=9C=AC=E5=A4=9A=E7=9A=AE=E8=82=A4=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dialogs/AnimationTracksEditorDialog.cs | 2 +- .../SkinManagerEditorDialog.Designer.cs | 139 ++++++++++++++++++ .../Dialogs/SkinManagerEditorDialog.cs | 45 ++++++ .../Dialogs/SkinManagerEditorDialog.resx | 120 +++++++++++++++ SpineViewer/Spine/AnimationTracks.cs | 6 +- .../Spine/Implementations/Spine/Spine21.cs | 21 +-- .../Spine/Implementations/Spine/Spine36.cs | 21 +-- .../Spine/Implementations/Spine/Spine37.cs | 18 ++- .../Spine/Implementations/Spine/Spine38.cs | 20 +-- .../Spine/Implementations/Spine/Spine40.cs | 20 +-- .../Spine/Implementations/Spine/Spine41.cs | 20 +-- .../Spine/Implementations/Spine/Spine42.cs | 20 +-- SpineViewer/Spine/SkinManager.cs | 115 +++++++++++++++ SpineViewer/Spine/Spine.cs | 105 +++++++++++-- SpineViewer/Spine/TypeConverter.cs | 33 +++++ SpineViewer/Spine/UITypeEditor.cs | 24 +++ 16 files changed, 648 insertions(+), 81 deletions(-) create mode 100644 SpineViewer/Dialogs/SkinManagerEditorDialog.Designer.cs create mode 100644 SpineViewer/Dialogs/SkinManagerEditorDialog.cs create mode 100644 SpineViewer/Dialogs/SkinManagerEditorDialog.resx create mode 100644 SpineViewer/Spine/SkinManager.cs diff --git a/SpineViewer/Dialogs/AnimationTracksEditorDialog.cs b/SpineViewer/Dialogs/AnimationTracksEditorDialog.cs index 647d1f3..a7fafb9 100644 --- a/SpineViewer/Dialogs/AnimationTracksEditorDialog.cs +++ b/SpineViewer/Dialogs/AnimationTracksEditorDialog.cs @@ -37,7 +37,7 @@ namespace SpineViewer.Dialogs spine.ClearTrack(tr.Index); } propertyGrid_AnimationTracks.Refresh(); - propertyGrid_AnimationTracks.SelectedGridItem = propertyGrid_AnimationTracks.SelectedGridItem.Parent.GridItems.Cast().Last(); + propertyGrid_AnimationTracks.SelectedGridItem = propertyGrid_AnimationTracks.SelectedGridItem?.Parent?.GridItems?.Cast().Last(); } } } diff --git a/SpineViewer/Dialogs/SkinManagerEditorDialog.Designer.cs b/SpineViewer/Dialogs/SkinManagerEditorDialog.Designer.cs new file mode 100644 index 0000000..7562a97 --- /dev/null +++ b/SpineViewer/Dialogs/SkinManagerEditorDialog.Designer.cs @@ -0,0 +1,139 @@ +namespace SpineViewer.Dialogs +{ + partial class SkinManagerEditorDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + panel = new Panel(); + tableLayoutPanel1 = new TableLayoutPanel(); + flowLayoutPanel1 = new FlowLayoutPanel(); + button_Add = new Button(); + button_Delete = new Button(); + propertyGrid_SkinManager = new PropertyGrid(); + panel.SuspendLayout(); + tableLayoutPanel1.SuspendLayout(); + flowLayoutPanel1.SuspendLayout(); + SuspendLayout(); + // + // panel + // + panel.Controls.Add(tableLayoutPanel1); + panel.Dock = DockStyle.Fill; + panel.Location = new Point(0, 0); + panel.Name = "panel"; + panel.Padding = new Padding(50, 15, 50, 10); + panel.Size = new Size(666, 483); + panel.TabIndex = 0; + // + // tableLayoutPanel1 + // + tableLayoutPanel1.ColumnCount = 1; + tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + tableLayoutPanel1.Controls.Add(flowLayoutPanel1, 0, 1); + tableLayoutPanel1.Controls.Add(propertyGrid_SkinManager, 0, 0); + tableLayoutPanel1.Dock = DockStyle.Fill; + tableLayoutPanel1.Location = new Point(50, 15); + tableLayoutPanel1.Name = "tableLayoutPanel1"; + tableLayoutPanel1.RowCount = 2; + tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); + tableLayoutPanel1.RowStyles.Add(new RowStyle()); + tableLayoutPanel1.Size = new Size(566, 458); + tableLayoutPanel1.TabIndex = 0; + // + // flowLayoutPanel1 + // + flowLayoutPanel1.AutoSize = true; + flowLayoutPanel1.Controls.Add(button_Add); + flowLayoutPanel1.Controls.Add(button_Delete); + flowLayoutPanel1.Dock = DockStyle.Fill; + flowLayoutPanel1.Location = new Point(3, 415); + flowLayoutPanel1.Name = "flowLayoutPanel1"; + flowLayoutPanel1.Size = new Size(560, 40); + flowLayoutPanel1.TabIndex = 2; + // + // button_Add + // + button_Add.Location = new Point(3, 3); + button_Add.Name = "button_Add"; + button_Add.Size = new Size(112, 34); + button_Add.TabIndex = 0; + button_Add.Text = "添加"; + button_Add.UseVisualStyleBackColor = true; + button_Add.Click += button_Add_Click; + // + // button_Delete + // + button_Delete.Location = new Point(121, 3); + button_Delete.Name = "button_Delete"; + button_Delete.Size = new Size(112, 34); + button_Delete.TabIndex = 1; + button_Delete.Text = "删除"; + button_Delete.UseVisualStyleBackColor = true; + button_Delete.Click += button_Delete_Click; + // + // propertyGrid_SkinManager + // + propertyGrid_SkinManager.Dock = DockStyle.Fill; + propertyGrid_SkinManager.HelpVisible = false; + propertyGrid_SkinManager.Location = new Point(3, 3); + propertyGrid_SkinManager.Name = "propertyGrid_SkinManager"; + propertyGrid_SkinManager.PropertySort = PropertySort.NoSort; + propertyGrid_SkinManager.Size = new Size(560, 406); + propertyGrid_SkinManager.TabIndex = 1; + propertyGrid_SkinManager.ToolbarVisible = false; + // + // SkinManagerEditorDialog + // + AutoScaleDimensions = new SizeF(11F, 24F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(666, 483); + Controls.Add(panel); + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + Name = "SkinManagerEditorDialog"; + ShowIcon = false; + ShowInTaskbar = false; + StartPosition = FormStartPosition.CenterScreen; + Text = "皮肤列表实时编辑器"; + panel.ResumeLayout(false); + tableLayoutPanel1.ResumeLayout(false); + tableLayoutPanel1.PerformLayout(); + flowLayoutPanel1.ResumeLayout(false); + ResumeLayout(false); + } + + #endregion + + private Panel panel; + private TableLayoutPanel tableLayoutPanel1; + private FlowLayoutPanel flowLayoutPanel1; + private Button button_Add; + private Button button_Delete; + private PropertyGrid propertyGrid_SkinManager; + } +} \ No newline at end of file diff --git a/SpineViewer/Dialogs/SkinManagerEditorDialog.cs b/SpineViewer/Dialogs/SkinManagerEditorDialog.cs new file mode 100644 index 0000000..87cc0da --- /dev/null +++ b/SpineViewer/Dialogs/SkinManagerEditorDialog.cs @@ -0,0 +1,45 @@ +using SpineViewer.Spine; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace SpineViewer.Dialogs +{ + public partial class SkinManagerEditorDialog : Form + { + private readonly Spine.Spine spine; + public SkinManagerEditorDialog(Spine.Spine spine) + { + InitializeComponent(); + this.spine = spine; + propertyGrid_SkinManager.SelectedObject = spine.SkinManager; + } + + private void button_Add_Click(object sender, EventArgs e) + { + if (spine.SkinNames.Count <= 0) + { + MessageBox.Info($"{spine.Name} 没有可用的皮肤"); + return; + } + spine.LoadSkin(spine.SkinNames[0]); + propertyGrid_SkinManager.Refresh(); + } + + private void button_Delete_Click(object sender, EventArgs e) + { + if (propertyGrid_SkinManager.SelectedGridItem?.Value is SkinWrapper sk) + spine.UnloadSkin(sk.Index); + propertyGrid_SkinManager.Refresh(); + + if (propertyGrid_SkinManager.SelectedGridItem?.Parent?.GridItems?.Cast().Last() is GridItem gt) + propertyGrid_SkinManager.SelectedGridItem = gt; + } + } +} diff --git a/SpineViewer/Dialogs/SkinManagerEditorDialog.resx b/SpineViewer/Dialogs/SkinManagerEditorDialog.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/SpineViewer/Dialogs/SkinManagerEditorDialog.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/SpineViewer/Spine/AnimationTracks.cs b/SpineViewer/Spine/AnimationTracks.cs index 448500a..c712061 100644 --- a/SpineViewer/Spine/AnimationTracks.cs +++ b/SpineViewer/Spine/AnimationTracks.cs @@ -33,14 +33,14 @@ namespace SpineViewer.Spine /// public override bool Equals(object? obj) { - if (obj is TrackWrapper tr) return ToString() == obj.ToString(); + if (obj is TrackWrapper) return ToString() == obj.ToString(); return base.Equals(obj); } /// /// 哈希码需要和 Equals 行为类似 /// - public override int GetHashCode() => ToString().GetHashCode(); + public override int GetHashCode() => (typeof(TrackWrapper).FullName + ToString()).GetHashCode(); } /// @@ -119,6 +119,6 @@ namespace SpineViewer.Spine return base.Equals(obj); } - public override int GetHashCode() => ToString().GetHashCode(); + public override int GetHashCode() => (typeof(AnimationTracksType).FullName + ToString()).GetHashCode(); } } diff --git a/SpineViewer/Spine/Implementations/Spine/Spine21.cs b/SpineViewer/Spine/Implementations/Spine/Spine21.cs index a4adf4a..09be4be 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine21.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine21.cs @@ -110,7 +110,6 @@ namespace SpineViewer.Spine.Implementations.Spine var fX = flipX; var fY = flipY; var animations = animationState.Tracks.Where(te => te is not null).Select(te => te.Animation.Name).ToArray(); - var sk = skin; var val = Math.Max(value, SCALE_MIN); if (skeletonBinary is not null) @@ -133,8 +132,8 @@ namespace SpineViewer.Spine.Implementations.Spine position = pos; flipX = fX; flipY = fY; + foreach (var s in loadedSkins) addSkin(s); for (int i = 0; i < animations.Length; i++) setAnimation(i, animations[i]); - skin = sk; } } @@ -160,15 +159,17 @@ namespace SpineViewer.Spine.Implementations.Spine set => skeleton.FlipY = value; } - protected override string skin + protected override void addSkin(string name) { - get => skeleton.Skin?.Name ?? "default"; - set - { - if (!skinNames.Contains(value)) return; - skeleton.SetSkin(value); - skeleton.SetSlotsToSetupPose(); - } + if (!skinNames.Contains(name)) return; + skeleton.SetSkin(name); // XXX: 3.7 及以下不支持 AddSkin + skeleton.SetSlotsToSetupPose(); + } + + protected override void clearSkin() + { + skeleton.SetSkin(skeletonData.DefaultSkin); + skeleton.SetSlotsToSetupPose(); } protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks[i] is not null).ToArray(); diff --git a/SpineViewer/Spine/Implementations/Spine/Spine36.cs b/SpineViewer/Spine/Implementations/Spine/Spine36.cs index abadf04..cc2cbe2 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine36.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine36.cs @@ -109,7 +109,6 @@ namespace SpineViewer.Spine.Implementations.Spine var fX = flipX; var fY = flipY; var animations = animationState.Tracks.Where(te => te is not null).Select(te => te.Animation.Name).ToArray(); - var sk = skin; var val = Math.Max(value, SCALE_MIN); if (skeletonBinary is not null) @@ -132,8 +131,8 @@ namespace SpineViewer.Spine.Implementations.Spine position = pos; flipX = fX; flipY = fY; + foreach (var s in loadedSkins) addSkin(s); for (int i = 0; i < animations.Length; i++) setAnimation(i, animations[i]); - skin = sk; } } @@ -159,15 +158,17 @@ namespace SpineViewer.Spine.Implementations.Spine set => skeleton.FlipY = value; } - protected override string skin + protected override void addSkin(string name) { - get => skeleton.Skin?.Name ?? "default"; - set - { - if (!skinNames.Contains(value)) return; - skeleton.SetSkin(value); - skeleton.SetSlotsToSetupPose(); - } + if (!skinNames.Contains(name)) return; + skeleton.SetSkin(name); // XXX: 3.7 及以下不支持 AddSkin + skeleton.SetSlotsToSetupPose(); + } + + protected override void clearSkin() + { + skeleton.SetSkin(skeletonData.DefaultSkin); + skeleton.SetSlotsToSetupPose(); } protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); diff --git a/SpineViewer/Spine/Implementations/Spine/Spine37.cs b/SpineViewer/Spine/Implementations/Spine/Spine37.cs index 9b17147..274be93 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine37.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine37.cs @@ -129,15 +129,17 @@ namespace SpineViewer.Spine.Implementations.Spine } } - protected override string skin + protected override void addSkin(string name) { - get => skeleton.Skin?.Name ?? "default"; - set - { - if (!skinNames.Contains(value)) return; - skeleton.SetSkin(value); - skeleton.SetSlotsToSetupPose(); - } + if (!skinNames.Contains(name)) return; + skeleton.SetSkin(name); // XXX: 3.7 及以下不支持 AddSkin + skeleton.SetSlotsToSetupPose(); + } + + protected override void clearSkin() + { + skeleton.SetSkin(skeletonData.DefaultSkin); + skeleton.SetSlotsToSetupPose(); } protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); diff --git a/SpineViewer/Spine/Implementations/Spine/Spine38.cs b/SpineViewer/Spine/Implementations/Spine/Spine38.cs index 09e1d8f..feb136a 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine38.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine38.cs @@ -82,7 +82,7 @@ namespace SpineViewer.Spine.Implementations.Spine foreach (var anime in skeletonData.Animations) animationNames.Add(anime.Name); - skeleton = new Skeleton(skeletonData); + skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器 animationStateData = new AnimationStateData(skeletonData); animationState = new AnimationState(animationStateData); } @@ -135,15 +135,17 @@ namespace SpineViewer.Spine.Implementations.Spine } } - protected override string skin + protected override void addSkin(string name) { - get => skeleton.Skin?.Name ?? "default"; - set - { - if (!skinNames.Contains(value)) return; - skeleton.SetSkin(value); - skeleton.SetSlotsToSetupPose(); - } + if (!skinNames.Contains(name)) return; + skeleton.Skin.AddSkin(skeletonData.FindSkin(name)); + skeleton.SetSlotsToSetupPose(); + } + + protected override void clearSkin() + { + skeleton.Skin.Clear(); + skeleton.SetSlotsToSetupPose(); } protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); diff --git a/SpineViewer/Spine/Implementations/Spine/Spine40.cs b/SpineViewer/Spine/Implementations/Spine/Spine40.cs index e6f3765..4a7ea42 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine40.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine40.cs @@ -78,7 +78,7 @@ namespace SpineViewer.Spine.Implementations.Spine foreach (var anime in skeletonData.Animations) animationNames.Add(anime.Name); - skeleton = new Skeleton(skeletonData); + skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器 animationStateData = new AnimationStateData(skeletonData); animationState = new AnimationState(animationStateData); } @@ -131,15 +131,17 @@ namespace SpineViewer.Spine.Implementations.Spine } } - protected override string skin + protected override void addSkin(string name) { - get => skeleton.Skin?.Name ?? "default"; - set - { - if (!skinNames.Contains(value)) return; - skeleton.SetSkin(value); - skeleton.SetSlotsToSetupPose(); - } + if (!skinNames.Contains(name)) return; + skeleton.Skin.AddSkin(skeletonData.FindSkin(name)); + skeleton.SetSlotsToSetupPose(); + } + + protected override void clearSkin() + { + skeleton.Skin.Clear(); + skeleton.SetSlotsToSetupPose(); } protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); diff --git a/SpineViewer/Spine/Implementations/Spine/Spine41.cs b/SpineViewer/Spine/Implementations/Spine/Spine41.cs index 52a5f68..c153110 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine41.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine41.cs @@ -78,7 +78,7 @@ namespace SpineViewer.Spine.Implementations.Spine foreach (var anime in skeletonData.Animations) animationNames.Add(anime.Name); - skeleton = new Skeleton(skeletonData); + skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器 animationStateData = new AnimationStateData(skeletonData); animationState = new AnimationState(animationStateData); } @@ -131,15 +131,17 @@ namespace SpineViewer.Spine.Implementations.Spine } } - protected override string skin + protected override void addSkin(string name) { - get => skeleton.Skin?.Name ?? "default"; - set - { - if (!skinNames.Contains(value)) return; - skeleton.SetSkin(value); - skeleton.SetSlotsToSetupPose(); - } + if (!skinNames.Contains(name)) return; + skeleton.Skin.AddSkin(skeletonData.FindSkin(name)); + skeleton.SetSlotsToSetupPose(); + } + + protected override void clearSkin() + { + skeleton.Skin.Clear(); + skeleton.SetSlotsToSetupPose(); } protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); diff --git a/SpineViewer/Spine/Implementations/Spine/Spine42.cs b/SpineViewer/Spine/Implementations/Spine/Spine42.cs index 46394e7..8225de0 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine42.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine42.cs @@ -78,7 +78,7 @@ namespace SpineViewer.Spine.Implementations.Spine foreach (var anime in skeletonData.Animations) animationNames.Add(anime.Name); - skeleton = new Skeleton(skeletonData); + skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器 animationStateData = new AnimationStateData(skeletonData); animationState = new AnimationState(animationStateData); } @@ -131,15 +131,17 @@ namespace SpineViewer.Spine.Implementations.Spine } } - protected override string skin + protected override void addSkin(string name) { - get => skeleton.Skin?.Name ?? "default"; - set - { - if (!skinNames.Contains(value)) return; - skeleton.SetSkin(value); - skeleton.SetSlotsToSetupPose(); - } + if (!skinNames.Contains(name)) return; + skeleton.Skin.AddSkin(skeletonData.FindSkin(name)); + skeleton.SetSlotsToSetupPose(); + } + + protected override void clearSkin() + { + skeleton.Skin.Clear(); + skeleton.SetSlotsToSetupPose(); } protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); diff --git a/SpineViewer/Spine/SkinManager.cs b/SpineViewer/Spine/SkinManager.cs new file mode 100644 index 0000000..f678b86 --- /dev/null +++ b/SpineViewer/Spine/SkinManager.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpineViewer.Spine +{ + /// + /// 对皮肤的包装类 + /// + [TypeConverter(typeof(SkinWrapperConverter))] + public class SkinWrapper(Spine spine, int i) + { + private readonly Spine spine = spine; + + [Browsable(false)] + public int Index { get; } = i; + + public override string ToString() + { + var loadedSkins = spine.GetLoadedSkins(); + if (Index >= 0 && Index < loadedSkins.Length) + return loadedSkins[Index]; + return "!NULL"; // XXX: 预期应该不会发生 + } + + public override bool Equals(object? obj) + { + if (obj is SkinWrapper) return ToString() == obj.ToString(); + return base.Equals(obj); + } + + public override int GetHashCode() => (typeof(SkinWrapper).FullName + ToString()).GetHashCode(); + } + + /// + /// 皮肤属性描述符, 实现对属性的读取和赋值 + /// + /// 关联的 Spine 对象 + public class SkinWrapperPropertyDescriptor(Spine spine, int i) : PropertyDescriptor($"Skin{i}", [new DisplayNameAttribute($"皮肤 {i}")]) + { + private readonly Spine spine = spine; + private readonly int idx = i; + + public override Type ComponentType => typeof(SkinManager); + public override bool IsReadOnly => false; + public override Type PropertyType => typeof(SkinWrapper); + public override bool CanResetValue(object component) => false; + public override void ResetValue(object component) { } + public override bool ShouldSerializeValue(object component) => false; + + /// + /// 得到一个 SkinWrapper, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性 + /// + public override object? GetValue(object? component) => new SkinWrapper(spine, idx); + + /// + /// 允许通过字符串赋值修改该位置的皮肤 + /// + public override void SetValue(object? component, object? value) + { + if (value is string s) spine.ReplaceSkin(idx, s); + } + } + + /// + /// SkinManager 动态类型包装类, 用于提供对 Spine 皮肤列表的管理能力 + /// + /// 关联的 Spine 对象 + public class SkinManager(Spine spine) : ICustomTypeDescriptor + { + private readonly Dictionary pdCache = []; + public Spine Spine { get; } = spine; + + // XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确 + + public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true); + public string? GetClassName() => TypeDescriptor.GetClassName(this, true); + public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true); + public TypeConverter? GetConverter() => TypeDescriptor.GetConverter(this, true); + public EventDescriptor? GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true); + public PropertyDescriptor? GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true); + public object? GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true); + public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(this, true); + public EventDescriptorCollection GetEvents(Attribute[]? attributes) => TypeDescriptor.GetEvents(this, attributes, true); + public object? GetPropertyOwner(PropertyDescriptor? pd) => this; + public PropertyDescriptorCollection GetProperties() => GetProperties(null); + public PropertyDescriptorCollection GetProperties(Attribute[]? attributes) + { + var props = new List(); + for (var i = 0; i < Spine.GetLoadedSkins().Length; i++) + { + if (!pdCache.ContainsKey(i)) + pdCache[i] = new SkinWrapperPropertyDescriptor(Spine, i); + props.Add(pdCache[i]); + } + return new PropertyDescriptorCollection(props.ToArray()); + } + + /// + /// 在属性面板悬停可以显示已加载的皮肤列表 + /// + public override string ToString() => $"[{string.Join(", ", Spine.GetLoadedSkins())}]"; + + public override bool Equals(object? obj) + { + if (obj is SkinManager manager) return ToString() == manager.ToString(); + return base.Equals(obj); + } + + public override int GetHashCode() => (typeof(SkinManager).FullName + ToString()).GetHashCode(); + } +} diff --git a/SpineViewer/Spine/Spine.cs b/SpineViewer/Spine/Spine.cs index 5601f42..db1c531 100644 --- a/SpineViewer/Spine/Spine.cs +++ b/SpineViewer/Spine/Spine.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Reflection; using System.Drawing.Design; +using NLog; namespace SpineViewer.Spine { @@ -11,6 +12,9 @@ namespace SpineViewer.Spine /// public abstract class Spine : ImplementationResolver, SFML.Graphics.Drawable, IDisposable { + private readonly Logger logger = LogManager.GetCurrentClassLogger(); + private bool skinLoggerWarned = false; + /// /// 空动画标记 /// @@ -64,6 +68,7 @@ namespace SpineViewer.Spine private Spine PostInit() { SkinNames = skinNames.AsReadOnly(); + SkinManager = new(this); AnimationNames = animationNames.AsReadOnly(); AnimationTracks = new(this); @@ -83,7 +88,7 @@ namespace SpineViewer.Spine Preview = tex.Texture.CopyToBitmap(); // 取最后一个作为初始, 尽可能去显示非默认的内容 - skin = SkinNames.Last(); + //skin = SkinNames.Last(); setAnimation(0, AnimationNames.Last()); return this; @@ -212,16 +217,12 @@ namespace SpineViewer.Spine #region 属性 | [3] 动画 /// - /// 使用的皮肤名称, 如果设置的皮肤不存在则忽略 + /// 已加载皮肤列表 /// - [TypeConverter(typeof(SkinConverter))] - [Category("[3] 动画"), DisplayName("皮肤")] - public string Skin - { - get { lock (_lock) return skin; } - set { lock (_lock) { skin = value; update(0); } } - } - protected abstract string skin { get; set; } + [Editor(typeof(SkinManagerEditor), typeof(UITypeEditor))] + [TypeConverter(typeof(ExpandableObjectConverter))] + [Category("[3] 动画"), DisplayName("已加载皮肤列表")] + public SkinManager SkinManager { get; private set; } /// /// 默认轨道动画名称, 如果设置的动画不存在则忽略 @@ -253,17 +254,95 @@ namespace SpineViewer.Spine /// [Browsable(false)] public ReadOnlyCollection SkinNames { get; private set; } - protected List skinNames = []; + protected readonly List skinNames = []; + + /// + /// 获取已加载的皮肤列表快照, 允许出现重复值 + /// + public string[] GetLoadedSkins() { lock (_lock) return loadedSkins.ToArray(); } + protected readonly List loadedSkins = []; + + /// + /// 加载指定皮肤, 添加至列表末尾, 如果不存在则忽略, 允许加载重复的值 + /// + public void LoadSkin(string name) + { + lock (_lock) + { + if (skinNames.Contains(name)) + { + loadedSkins.Add(name); + reloadSkins(); + + if (!skinLoggerWarned && Version <= SpineVersion.V37 && loadedSkins.Count > 1) + { + logger.Warn($"Multiplt skins not supported in SpineVersion {Version.GetName()}"); + skinLoggerWarned = true; + } + } + } + } + + /// + /// 卸载列表指定位置皮肤, 如果超出范围则忽略 + /// + public void UnloadSkin(int idx) + { + lock (_lock) + { + if (idx >= 0 && idx < loadedSkins.Count) + { + loadedSkins.RemoveAt(idx); + reloadSkins(); + } + } + } + + /// + /// 替换皮肤列表指定位置皮肤, 超出范围或者皮肤不存在则忽略 + /// + public void ReplaceSkin(int idx, string name) + { + lock (_lock) + { + if (idx >= 0 && idx < loadedSkins.Count && skinNames.Contains(name)) + { + loadedSkins[idx] = name; + reloadSkins(); + } + } + } + + /// + /// 重新加载现有皮肤列表, 用于刷新等操作 + /// + public void ReloadSkins() { lock (_lock) reloadSkins(); } + private void reloadSkins() + { + clearSkin(); + foreach (var s in loadedSkins) addSkin(s); + update(0); + } + + /// + /// 加载皮肤, 之后需要使用 来复位 + /// + protected abstract void addSkin(string name); + + /// + /// 清空加载的所有皮肤 + /// + protected abstract void clearSkin(); /// /// 包含的所有动画名称 /// [Browsable(false)] public ReadOnlyCollection AnimationNames { get; private set; } - protected List animationNames = [EMPTY_ANIMATION]; + protected readonly List animationNames = [EMPTY_ANIMATION]; /// - /// 获取所有非 null 的轨道索引 + /// 获取所有非 null 的轨道索引快照 /// public int[] GetTrackIndices() { lock (_lock) return getTrackIndices(); } protected abstract int[] getTrackIndices(); diff --git a/SpineViewer/Spine/TypeConverter.cs b/SpineViewer/Spine/TypeConverter.cs index f13ddd2..e4b5114 100644 --- a/SpineViewer/Spine/TypeConverter.cs +++ b/SpineViewer/Spine/TypeConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; @@ -107,4 +108,36 @@ namespace SpineViewer.Spine return base.GetStandardValues(context); } } + + public class SkinWrapperConverter : StringConverter + { + // NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转 + // ToString 实现了 ConvertTo + // SetValue 实现了从字符串设置属性 + + public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true; + + public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true; + + public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context) + { + if (context.Instance is SkinManager manager) + { + return new StandardValuesCollection(manager.Spine.SkinNames); + } + else if (context.Instance is object[] instances && instances.All(x => x is SkinManager)) + { + // XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的 SkinManager[] 类型 + var managers = instances.Cast().ToArray(); + if (managers.Length > 0) + { + IEnumerable common = managers[0].Spine.SkinNames; + foreach (var t in managers.Skip(1)) + common = common.Union(t.Spine.SkinNames); + return new StandardValuesCollection(common.ToArray()); + } + } + return base.GetStandardValues(context); + } + } } diff --git a/SpineViewer/Spine/UITypeEditor.cs b/SpineViewer/Spine/UITypeEditor.cs index 218cd18..d8dcd1c 100644 --- a/SpineViewer/Spine/UITypeEditor.cs +++ b/SpineViewer/Spine/UITypeEditor.cs @@ -61,4 +61,28 @@ namespace SpineViewer.Spine return value; } } + + /// + /// 多轨道动画编辑器 + /// + public class SkinManagerEditor : UITypeEditor + { + public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context) => UITypeEditorEditStyle.Modal; + + public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value) + { + if (provider == null || context == null || context.Instance is not Spine) + return value; + + IWindowsFormsEditorService editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService; + if (editorService == null) + return value; + + using (var dialog = new SkinManagerEditorDialog((Spine)context.Instance)) + editorService.ShowDialog(dialog); + + TypeDescriptor.Refresh(context.Instance); + return value; + } + } }