增加3.8及以上版本多皮肤支持

This commit is contained in:
ww-rm
2025-04-05 00:57:04 +08:00
parent 2c846c0db9
commit 204dcd6498
16 changed files with 648 additions and 81 deletions

View File

@@ -37,7 +37,7 @@ namespace SpineViewer.Dialogs
spine.ClearTrack(tr.Index); spine.ClearTrack(tr.Index);
} }
propertyGrid_AnimationTracks.Refresh(); propertyGrid_AnimationTracks.Refresh();
propertyGrid_AnimationTracks.SelectedGridItem = propertyGrid_AnimationTracks.SelectedGridItem.Parent.GridItems.Cast<GridItem>().Last(); propertyGrid_AnimationTracks.SelectedGridItem = propertyGrid_AnimationTracks.SelectedGridItem?.Parent?.GridItems?.Cast<GridItem>().Last();
} }
} }
} }

View File

@@ -0,0 +1,139 @@
namespace SpineViewer.Dialogs
{
partial class SkinManagerEditorDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
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;
}
}

View File

@@ -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<GridItem>().Last() is GridItem gt)
propertyGrid_SkinManager.SelectedGridItem = gt;
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -33,14 +33,14 @@ namespace SpineViewer.Spine
/// </summary> /// </summary>
public override bool Equals(object? obj) 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); return base.Equals(obj);
} }
/// <summary> /// <summary>
/// 哈希码需要和 Equals 行为类似 /// 哈希码需要和 Equals 行为类似
/// </summary> /// </summary>
public override int GetHashCode() => ToString().GetHashCode(); public override int GetHashCode() => (typeof(TrackWrapper).FullName + ToString()).GetHashCode();
} }
/// <summary> /// <summary>
@@ -119,6 +119,6 @@ namespace SpineViewer.Spine
return base.Equals(obj); return base.Equals(obj);
} }
public override int GetHashCode() => ToString().GetHashCode(); public override int GetHashCode() => (typeof(AnimationTracksType).FullName + ToString()).GetHashCode();
} }
} }

View File

@@ -110,7 +110,6 @@ namespace SpineViewer.Spine.Implementations.Spine
var fX = flipX; var fX = flipX;
var fY = flipY; var fY = flipY;
var animations = animationState.Tracks.Where(te => te is not null).Select(te => te.Animation.Name).ToArray(); 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); var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null) if (skeletonBinary is not null)
@@ -133,8 +132,8 @@ namespace SpineViewer.Spine.Implementations.Spine
position = pos; position = pos;
flipX = fX; flipX = fX;
flipY = fY; flipY = fY;
foreach (var s in loadedSkins) addSkin(s);
for (int i = 0; i < animations.Length; i++) setAnimation(i, animations[i]); 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; set => skeleton.FlipY = value;
} }
protected override string skin protected override void addSkin(string name)
{ {
get => skeleton.Skin?.Name ?? "default"; if (!skinNames.Contains(name)) return;
set skeleton.SetSkin(name); // XXX: 3.7 及以下不支持 AddSkin
{ skeleton.SetSlotsToSetupPose();
if (!skinNames.Contains(value)) return; }
skeleton.SetSkin(value);
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(); protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks[i] is not null).ToArray();

View File

@@ -109,7 +109,6 @@ namespace SpineViewer.Spine.Implementations.Spine
var fX = flipX; var fX = flipX;
var fY = flipY; var fY = flipY;
var animations = animationState.Tracks.Where(te => te is not null).Select(te => te.Animation.Name).ToArray(); 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); var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null) if (skeletonBinary is not null)
@@ -132,8 +131,8 @@ namespace SpineViewer.Spine.Implementations.Spine
position = pos; position = pos;
flipX = fX; flipX = fX;
flipY = fY; flipY = fY;
foreach (var s in loadedSkins) addSkin(s);
for (int i = 0; i < animations.Length; i++) setAnimation(i, animations[i]); 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; set => skeleton.FlipY = value;
} }
protected override string skin protected override void addSkin(string name)
{ {
get => skeleton.Skin?.Name ?? "default"; if (!skinNames.Contains(name)) return;
set skeleton.SetSkin(name); // XXX: 3.7 及以下不支持 AddSkin
{ skeleton.SetSlotsToSetupPose();
if (!skinNames.Contains(value)) return; }
skeleton.SetSkin(value);
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(); protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();

View File

@@ -129,15 +129,17 @@ namespace SpineViewer.Spine.Implementations.Spine
} }
} }
protected override string skin protected override void addSkin(string name)
{ {
get => skeleton.Skin?.Name ?? "default"; if (!skinNames.Contains(name)) return;
set skeleton.SetSkin(name); // XXX: 3.7 及以下不支持 AddSkin
{ skeleton.SetSlotsToSetupPose();
if (!skinNames.Contains(value)) return; }
skeleton.SetSkin(value);
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(); protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();

View File

@@ -82,7 +82,7 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var anime in skeletonData.Animations) foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name); animationNames.Add(anime.Name);
skeleton = new Skeleton(skeletonData); skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
animationStateData = new AnimationStateData(skeletonData); animationStateData = new AnimationStateData(skeletonData);
animationState = new AnimationState(animationStateData); 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"; if (!skinNames.Contains(name)) return;
set skeleton.Skin.AddSkin(skeletonData.FindSkin(name));
{ skeleton.SetSlotsToSetupPose();
if (!skinNames.Contains(value)) return; }
skeleton.SetSkin(value);
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(); protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();

View File

@@ -78,7 +78,7 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var anime in skeletonData.Animations) foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name); animationNames.Add(anime.Name);
skeleton = new Skeleton(skeletonData); skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
animationStateData = new AnimationStateData(skeletonData); animationStateData = new AnimationStateData(skeletonData);
animationState = new AnimationState(animationStateData); 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"; if (!skinNames.Contains(name)) return;
set skeleton.Skin.AddSkin(skeletonData.FindSkin(name));
{ skeleton.SetSlotsToSetupPose();
if (!skinNames.Contains(value)) return; }
skeleton.SetSkin(value);
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(); protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();

View File

@@ -78,7 +78,7 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var anime in skeletonData.Animations) foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name); animationNames.Add(anime.Name);
skeleton = new Skeleton(skeletonData); skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
animationStateData = new AnimationStateData(skeletonData); animationStateData = new AnimationStateData(skeletonData);
animationState = new AnimationState(animationStateData); 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"; if (!skinNames.Contains(name)) return;
set skeleton.Skin.AddSkin(skeletonData.FindSkin(name));
{ skeleton.SetSlotsToSetupPose();
if (!skinNames.Contains(value)) return; }
skeleton.SetSkin(value);
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(); protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();

View File

@@ -78,7 +78,7 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var anime in skeletonData.Animations) foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name); animationNames.Add(anime.Name);
skeleton = new Skeleton(skeletonData); skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
animationStateData = new AnimationStateData(skeletonData); animationStateData = new AnimationStateData(skeletonData);
animationState = new AnimationState(animationStateData); 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"; if (!skinNames.Contains(name)) return;
set skeleton.Skin.AddSkin(skeletonData.FindSkin(name));
{ skeleton.SetSlotsToSetupPose();
if (!skinNames.Contains(value)) return; }
skeleton.SetSkin(value);
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(); protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();

View File

@@ -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
{
/// <summary>
/// 对皮肤的包装类
/// </summary>
[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();
}
/// <summary>
/// 皮肤属性描述符, 实现对属性的读取和赋值
/// </summary>
/// <param name="spine">关联的 Spine 对象</param>
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;
/// <summary>
/// 得到一个 SkinWrapper, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
/// </summary>
public override object? GetValue(object? component) => new SkinWrapper(spine, idx);
/// <summary>
/// 允许通过字符串赋值修改该位置的皮肤
/// </summary>
public override void SetValue(object? component, object? value)
{
if (value is string s) spine.ReplaceSkin(idx, s);
}
}
/// <summary>
/// SkinManager 动态类型包装类, 用于提供对 Spine 皮肤列表的管理能力
/// </summary>
/// <param name="spine">关联的 Spine 对象</param>
public class SkinManager(Spine spine) : ICustomTypeDescriptor
{
private readonly Dictionary<int, SkinWrapperPropertyDescriptor> 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<SkinWrapperPropertyDescriptor>();
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());
}
/// <summary>
/// 在属性面板悬停可以显示已加载的皮肤列表
/// </summary>
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();
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Reflection; using System.Reflection;
using System.Drawing.Design; using System.Drawing.Design;
using NLog;
namespace SpineViewer.Spine namespace SpineViewer.Spine
{ {
@@ -11,6 +12,9 @@ namespace SpineViewer.Spine
/// </summary> /// </summary>
public abstract class Spine : ImplementationResolver<Spine, SpineImplementationAttribute, SpineVersion>, SFML.Graphics.Drawable, IDisposable public abstract class Spine : ImplementationResolver<Spine, SpineImplementationAttribute, SpineVersion>, SFML.Graphics.Drawable, IDisposable
{ {
private readonly Logger logger = LogManager.GetCurrentClassLogger();
private bool skinLoggerWarned = false;
/// <summary> /// <summary>
/// 空动画标记 /// 空动画标记
/// </summary> /// </summary>
@@ -64,6 +68,7 @@ namespace SpineViewer.Spine
private Spine PostInit() private Spine PostInit()
{ {
SkinNames = skinNames.AsReadOnly(); SkinNames = skinNames.AsReadOnly();
SkinManager = new(this);
AnimationNames = animationNames.AsReadOnly(); AnimationNames = animationNames.AsReadOnly();
AnimationTracks = new(this); AnimationTracks = new(this);
@@ -83,7 +88,7 @@ namespace SpineViewer.Spine
Preview = tex.Texture.CopyToBitmap(); Preview = tex.Texture.CopyToBitmap();
// 取最后一个作为初始, 尽可能去显示非默认的内容 // 取最后一个作为初始, 尽可能去显示非默认的内容
skin = SkinNames.Last(); //skin = SkinNames.Last();
setAnimation(0, AnimationNames.Last()); setAnimation(0, AnimationNames.Last());
return this; return this;
@@ -212,16 +217,12 @@ namespace SpineViewer.Spine
#region | [3] #region | [3]
/// <summary> /// <summary>
/// 使用的皮肤名称, 如果设置的皮肤不存在则忽略 /// 已加载皮肤列表
/// </summary> /// </summary>
[TypeConverter(typeof(SkinConverter))] [Editor(typeof(SkinManagerEditor), typeof(UITypeEditor))]
[Category("[3] "), DisplayName("")] [TypeConverter(typeof(ExpandableObjectConverter))]
public string Skin [Category("[3] "), DisplayName("")]
{ public SkinManager SkinManager { get; private set; }
get { lock (_lock) return skin; }
set { lock (_lock) { skin = value; update(0); } }
}
protected abstract string skin { get; set; }
/// <summary> /// <summary>
/// 默认轨道动画名称, 如果设置的动画不存在则忽略 /// 默认轨道动画名称, 如果设置的动画不存在则忽略
@@ -253,17 +254,95 @@ namespace SpineViewer.Spine
/// </summary> /// </summary>
[Browsable(false)] [Browsable(false)]
public ReadOnlyCollection<string> SkinNames { get; private set; } public ReadOnlyCollection<string> SkinNames { get; private set; }
protected List<string> skinNames = []; protected readonly List<string> skinNames = [];
/// <summary>
/// 获取已加载的皮肤列表快照, 允许出现重复值
/// </summary>
public string[] GetLoadedSkins() { lock (_lock) return loadedSkins.ToArray(); }
protected readonly List<string> loadedSkins = [];
/// <summary>
/// 加载指定皮肤, 添加至列表末尾, 如果不存在则忽略, 允许加载重复的值
/// </summary>
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;
}
}
}
}
/// <summary>
/// 卸载列表指定位置皮肤, 如果超出范围则忽略
/// </summary>
public void UnloadSkin(int idx)
{
lock (_lock)
{
if (idx >= 0 && idx < loadedSkins.Count)
{
loadedSkins.RemoveAt(idx);
reloadSkins();
}
}
}
/// <summary>
/// 替换皮肤列表指定位置皮肤, 超出范围或者皮肤不存在则忽略
/// </summary>
public void ReplaceSkin(int idx, string name)
{
lock (_lock)
{
if (idx >= 0 && idx < loadedSkins.Count && skinNames.Contains(name))
{
loadedSkins[idx] = name;
reloadSkins();
}
}
}
/// <summary>
/// 重新加载现有皮肤列表, 用于刷新等操作
/// </summary>
public void ReloadSkins() { lock (_lock) reloadSkins(); }
private void reloadSkins()
{
clearSkin();
foreach (var s in loadedSkins) addSkin(s);
update(0);
}
/// <summary>
/// 加载皮肤, 之后需要使用 <see cref="setSlotsToSetupPose"/> 来复位
/// </summary>
protected abstract void addSkin(string name);
/// <summary>
/// 清空加载的所有皮肤
/// </summary>
protected abstract void clearSkin();
/// <summary> /// <summary>
/// 包含的所有动画名称 /// 包含的所有动画名称
/// </summary> /// </summary>
[Browsable(false)] [Browsable(false)]
public ReadOnlyCollection<string> AnimationNames { get; private set; } public ReadOnlyCollection<string> AnimationNames { get; private set; }
protected List<string> animationNames = [EMPTY_ANIMATION]; protected readonly List<string> animationNames = [EMPTY_ANIMATION];
/// <summary> /// <summary>
/// 获取所有非 null 的轨道索引 /// 获取所有非 null 的轨道索引快照
/// </summary> /// </summary>
public int[] GetTrackIndices() { lock (_lock) return getTrackIndices(); } public int[] GetTrackIndices() { lock (_lock) return getTrackIndices(); }
protected abstract int[] getTrackIndices(); protected abstract int[] getTrackIndices();

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@@ -107,4 +108,36 @@ namespace SpineViewer.Spine
return base.GetStandardValues(context); 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<SkinManager>().ToArray();
if (managers.Length > 0)
{
IEnumerable<string> 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);
}
}
} }

View File

@@ -61,4 +61,28 @@ namespace SpineViewer.Spine
return value; return value;
} }
} }
/// <summary>
/// 多轨道动画编辑器
/// </summary>
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;
}
}
} }