Compare commits

...

46 Commits

Author SHA1 Message Date
ww-rm
4b23c779d3 Merge pull request #124 from ww-rm/dev/wpf
v0.16.7
2025-10-06 14:04:13 +08:00
ww-rm
f5684a50dc update to v0.16.7 2025-10-06 14:03:19 +08:00
ww-rm
579ce9f944 update changelog 2025-10-06 14:03:03 +08:00
ww-rm
7aa88089b8 修复空帧导致的包围盒计算错误 2025-10-06 14:02:21 +08:00
ww-rm
be983f8407 修复窗口二次显示错误 2025-10-06 13:32:03 +08:00
ww-rm
249b930602 Merge pull request #122 from ww-rm/dev/wpf
v0.16.6
2025-10-04 20:55:59 +08:00
ww-rm
6472f378b7 update to v0.16.6 2025-10-04 20:53:29 +08:00
ww-rm
8672f0571c udpate changelog 2025-10-04 20:53:06 +08:00
ww-rm
e7a990c1bd 修复可能出现的0缩放错误 2025-10-04 20:50:08 +08:00
ww-rm
6727fa8e8f Merge pull request #120 from ww-rm/dev/wpf
v0.16.5
2025-10-04 16:59:08 +08:00
ww-rm
66d8c489b5 update to v0.16.5 2025-10-04 16:58:32 +08:00
ww-rm
1931c4713a update changelog 2025-10-04 16:58:09 +08:00
ww-rm
f19f172e7c 修复窗口联动显示问题 2025-10-04 16:56:32 +08:00
ww-rm
092fa76124 修复对于某些旧atlas没有size行的读取异常 2025-10-04 16:38:14 +08:00
ww-rm
a0b7db0a70 Merge pull request #119 from ww-rm/dev/wpf
v0.16.4
2025-10-04 00:11:15 +08:00
ww-rm
6438b46ea0 修复样式错误 2025-10-04 00:09:38 +08:00
ww-rm
2bf73db9d3 补充前景色绑定 2025-10-03 23:54:54 +08:00
ww-rm
03c4974c9f Merge pull request #118 from ww-rm/dev/wpf
v0.16.4
2025-10-03 23:50:05 +08:00
ww-rm
760fa3a451 update to v0.16.4 2025-10-03 23:49:12 +08:00
ww-rm
018d8f5330 update changelog 2025-10-03 23:48:25 +08:00
ww-rm
c9730e1a11 完善标题栏皮肤颜色切换 2025-10-03 23:45:35 +08:00
ww-rm
1f6e19e544 修改日志着色 2025-10-03 22:29:43 +08:00
ww-rm
a1a0777791 重构样式 2025-10-03 22:22:21 +08:00
ww-rm
887e3f76d2 增加程序皮肤首选项 2025-10-03 19:38:48 +08:00
ww-rm
8b622050fa 重构 2025-10-03 19:11:59 +08:00
ww-rm
20369aaf43 调整颜色按钮背景色固定白色 2025-10-03 16:17:26 +08:00
ww-rm
07c0e84b7d 修复颜色错误 2025-10-03 16:11:04 +08:00
ww-rm
6770acaffd small change 2025-10-03 13:34:45 +08:00
ww-rm
6201ccc7d1 去除独立颜色使用 2025-10-03 09:46:14 +08:00
ww-rm
965d1c469e 增加ColorPicker使用 2025-10-03 09:19:02 +08:00
ww-rm
b448ca8cb0 重构 2025-10-02 22:42:21 +08:00
ww-rm
2204eb6c75 增加apng格式并且调整部分布局结构 2025-10-02 22:18:05 +08:00
ww-rm
0abe063899 调整文件结构 2025-10-02 15:18:40 +08:00
ww-rm
6f9b357473 Merge pull request #116 from ww-rm/dev/wpf
v0.16.3
2025-10-02 14:22:11 +08:00
ww-rm
152d842043 update to v0.16.3 2025-10-02 14:21:35 +08:00
ww-rm
d16f97d574 update changelog 2025-10-02 14:21:18 +08:00
ww-rm
d28eabaca5 解决可能卡死问题 2025-10-02 14:19:53 +08:00
ww-rm
b730f677be 完善像素检测 2025-10-02 14:04:42 +08:00
ww-rm
8f8806417a 完善工作区加载模型逻辑 2025-10-02 11:44:51 +08:00
ww-rm
06694c9e89 补充注释 2025-10-02 11:29:13 +08:00
ww-rm
e9b0ce3db2 增加None命中测试等级 2025-10-02 11:19:22 +08:00
ww-rm
4c72608398 修改部分布局 2025-10-02 11:01:38 +08:00
ww-rm
7e99882fbf 增加命中测试等级选项 2025-10-02 10:32:24 +08:00
ww-rm
0d72d8749a 调整调试输出逻辑 2025-10-02 10:23:51 +08:00
ww-rm
d5b7a74520 增加Name属性 2025-10-02 10:17:12 +08:00
ww-rm
0202027edb 修复模型添加顺序错误 2025-10-02 09:32:17 +08:00
62 changed files with 2108 additions and 1087 deletions

View File

@@ -1,5 +1,32 @@
# CHANGELOG
## v0.16.7
- 修复空帧导致的包围盒计算错误
- 修复重复启动程序无法唤出界面的问题
## v0.16.6
- 修复控件尺寸为0时导致的画面缩放错误
## v0.16.5
- 修复对于无 size 行的旧 atlas 格式读取错误
- 修复托盘化之后无法联动显示窗口的问题
## v0.16.4
- 增加 apng 导出格式
- 增加颜色拾取器面板
- 增加程序皮肤(主题颜色)首选项
- 优化部分使用体验
## v0.16.3
- 修复加载工作区时的顺序错误
- 调整部分调试渲染的逻辑
- 完善命中检测逻辑
## v0.16.2
- 修复批量添加时的添加顺序错误

View File

@@ -240,8 +240,8 @@ namespace SFMLRenderer
if (RenderWindow is null) return;
float parentW = (float)sizeInfo.NewSize.Width;
float parentH = (float)sizeInfo.NewSize.Height;
float renderW = (float)_hwndHost.ActualWidth;
float renderH = (float)_hwndHost.ActualHeight;
float renderW = _resolution.X;
float renderH = _resolution.Y;
float scale = Math.Min(parentW / renderW, parentH / renderH); // 两方向取较小值, 保证 parent 覆盖 render
renderW *= scale;
renderH *= scale;

View File

@@ -7,7 +7,7 @@
<TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.16.0</Version>
<Version>0.16.6</Version>
<UseWPF>true</UseWPF>
</PropertyGroup>

View File

@@ -28,6 +28,7 @@ namespace Spine.Exporters
{
Gif,
Webp,
Apng,
Mp4,
Webm,
Mkv,
@@ -41,31 +42,37 @@ namespace Spine.Exporters
private VideoFormat _format = VideoFormat.Mp4;
/// <summary>
/// 动图是否循环 [Gif/Webp]
/// [Gif/Webp/Apng] 动图是否循环
/// </summary>
public bool Loop { get => _loop; set => _loop = value; }
private bool _loop = true;
/// <summary>
/// 质量 [Webp]
/// [Webp] 质量
/// </summary>
public int Quality { get => _quality; set => _quality = Math.Clamp(value, 0, 100); }
private int _quality = 75;
/// <summary>
/// 无损压缩 [Webp]
/// [Webp] 无损压缩
/// </summary>
public bool Lossless { get => _lossless; set => _lossless = value; }
private bool _lossless = false;
/// <summary>
/// CRF [Mp4/Webm/Mkv]
/// [Apng] 预测器算法, 取值范围 0-5, 分别对应 none, sub, up, avg, paeth, mixed
/// </summary>
public int ApngPred { get => _apngPred; set => _apngPred = Math.Clamp(value, 0, 5); }
private int _apngPred = 5;
/// <summary>
/// [Mp4/Webm/Mkv] CRF
/// </summary>
public int Crf { get => _crf; set => _crf = Math.Clamp(value, 0, 63); }
private int _crf = 23;
/// <summary>
/// prores_ks 编码器的配置等级, -1 是自动, 越高质量越好, 只有 4 及以上才有透明通道 [Mov]
/// [Mov] prores_ks 编码器的配置等级, -1 是自动, 越高质量越好, 只有 4 及以上才有透明通道
/// </summary>
public int Profile { get => _profile; set => _profile = Math.Clamp(value, -1, 5); }
private int _profile = 5;
@@ -93,6 +100,7 @@ namespace Spine.Exporters
{
VideoFormat.Gif => SetGifOptions,
VideoFormat.Webp => SetWebpOptions,
VideoFormat.Apng => SetApngOptions,
VideoFormat.Mp4 => SetMp4Options,
VideoFormat.Webm => SetWebmOptions,
VideoFormat.Mkv => SetMkvOptions,
@@ -132,6 +140,13 @@ namespace Spine.Exporters
.WithCustomArgument(customArgs);
}
private void SetApngOptions(FFMpegArgumentOptions options)
{
var customArgs = $"-vf unpremultiply=inplace=1 -plays {(_loop ? 0 : 1)} -pred {_apngPred}";
options.ForceFormat("apng").WithVideoCodec("apng").ForcePixelFormat("rgba")
.WithCustomArgument(customArgs);
}
private void SetMp4Options(FFMpegArgumentOptions options)
{
// XXX: windows 默认播放器在播放 MP4 格式时对于 libx264 编码器只支持 yuv420p 的像素格式

View File

@@ -112,6 +112,14 @@ namespace Spine.Implementations
if (ForceMipmap) texture.GenerateMipmap();
page.rendererObject = texture;
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
if (page.width <= 0 || page.height <= 0)
{
var texSize = texture.Size;
page.width = (int)texSize.X;
page.height = (int)texSize.Y;
}
}
public virtual void Load(SpineRuntime34.AtlasPage page, string path)
@@ -147,6 +155,14 @@ namespace Spine.Implementations
if (ForceMipmap) texture.GenerateMipmap();
page.rendererObject = texture;
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
if (page.width <= 0 || page.height <= 0)
{
var texSize = texture.Size;
page.width = (int)texSize.X;
page.height = (int)texSize.Y;
}
}
public virtual void Load(SpineRuntime35.AtlasPage page, string path)
@@ -182,6 +198,14 @@ namespace Spine.Implementations
if (ForceMipmap) texture.GenerateMipmap();
page.rendererObject = texture;
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
if (page.width <= 0 || page.height <= 0)
{
var texSize = texture.Size;
page.width = (int)texSize.X;
page.height = (int)texSize.Y;
}
}
public virtual void Load(SpineRuntime36.AtlasPage page, string path)
@@ -217,6 +241,14 @@ namespace Spine.Implementations
if (ForceMipmap) texture.GenerateMipmap();
page.rendererObject = texture;
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
if (page.width <= 0 || page.height <= 0)
{
var texSize = texture.Size;
page.width = (int)texSize.X;
page.height = (int)texSize.Y;
}
}
public virtual void Load(SpineRuntime37.AtlasPage page, string path)
@@ -252,6 +284,14 @@ namespace Spine.Implementations
if (ForceMipmap) texture.GenerateMipmap();
page.rendererObject = texture;
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
if (page.width <= 0 || page.height <= 0)
{
var texSize = texture.Size;
page.width = (int)texSize.X;
page.height = (int)texSize.Y;
}
}
public virtual void Load(SpineRuntime38.AtlasPage page, string path)
@@ -288,9 +328,13 @@ namespace Spine.Implementations
page.rendererObject = texture;
// 似乎是不需要设置的, 因为存在某些 png 和 atlas 大小不同的情况, 一般是有一些缩放, 如果设置了反而渲染异常
// page.width = (int)texture.Size.X;
// page.height = (int)texture.Size.Y;
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
if (page.width <= 0 || page.height <= 0)
{
var texSize = texture.Size;
page.width = (int)texSize.X;
page.height = (int)texSize.Y;
}
}
public virtual void Load(SpineRuntime40.AtlasPage page, string path)
@@ -326,6 +370,14 @@ namespace Spine.Implementations
if (ForceMipmap) texture.GenerateMipmap();
page.rendererObject = texture;
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
if (page.width <= 0 || page.height <= 0)
{
var texSize = texture.Size;
page.width = (int)texSize.X;
page.height = (int)texSize.Y;
}
}
public virtual void Load(SpineRuntime41.AtlasPage page, string path)
@@ -361,6 +413,14 @@ namespace Spine.Implementations
if (ForceMipmap) texture.GenerateMipmap();
page.rendererObject = texture;
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
if (page.width <= 0 || page.height <= 0)
{
var texSize = texture.Size;
page.width = (int)texSize.X;
page.height = (int)texSize.Y;
}
}
public virtual void Load(SpineRuntime42.AtlasPage page, string path)
@@ -396,6 +456,14 @@ namespace Spine.Implementations
if (ForceMipmap) texture.GenerateMipmap();
page.rendererObject = texture;
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
if (page.width <= 0 || page.height <= 0)
{
var texSize = texture.Size;
page.width = (int)texSize.X;
page.height = (int)texSize.Y;
}
}
public virtual void Unload(object texture)

View File

@@ -52,6 +52,7 @@ namespace Spine.Implementations.V21
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -52,6 +52,7 @@ namespace Spine.Implementations.V34
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -52,6 +52,7 @@ namespace Spine.Implementations.V35
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -52,6 +52,7 @@ namespace Spine.Implementations.V36
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -52,6 +52,7 @@ namespace Spine.Implementations.V37
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -52,6 +52,7 @@ namespace Spine.Implementations.V38
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -52,6 +52,7 @@ namespace Spine.Implementations.V40
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -52,6 +52,7 @@ namespace Spine.Implementations.V41
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -60,6 +60,7 @@ namespace Spine.Implementations.V42
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -15,6 +15,11 @@ namespace Spine.Interfaces
/// </summary>
public enum Physics { None, Reset, Update, Pose }
/// <summary>
/// 名称
/// </summary>
public string Name { get; }
/// <summary>
/// R
/// </summary>

View File

@@ -7,7 +7,7 @@
<TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.16.2</Version>
<Version>0.16.7</Version>
</PropertyGroup>
<PropertyGroup>

View File

@@ -1,4 +1,5 @@
using SkiaSharp;
using NLog;
using Spine.Interfaces;
using Spine.Interfaces.Attachments;
using System;
using System.Collections.Generic;
@@ -6,10 +7,35 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Interfaces
namespace Spine
{
/// <summary>
/// 命中测试等级枚举值
/// </summary>
public enum HitTestLevel { None, Bounds, Meshes, Pixels }
public static class SpineExtension
{
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
private static readonly SFML.Graphics.RenderTexture _renderTex; // XXX: 在此保留一个静态变量, 并且没有使用 Dispose 进行资源释放
static SpineExtension()
{
_renderTex = new(1, 1);
_renderTex.SetActive(false);
}
/// <summary>
/// 命中检测精确度等级
/// </summary>
public static HitTestLevel HitTestLevel { get; set; }
/// <summary>
/// 命中测试时输出命中的插槽名称
/// </summary>
public static bool LogHitSlots { get; set; }
/// <summary>
/// 获取当前状态包围盒
/// </summary>
@@ -84,38 +110,58 @@ namespace Spine.Interfaces
break;
}
for (int ii = 0; ii + 1 < verticesLength; ii += 2)
if (verticesLength > 0)
{
float vx = vertices[ii];
float vy = vertices[ii + 1];
minX = Math.Min(minX, vx);
minY = Math.Min(minY, vy);
maxX = Math.Max(maxX, vx);
maxY = Math.Max(maxY, vy);
for (int ii = 0; ii + 1 < verticesLength; ii += 2)
{
float vx = vertices[ii];
float vy = vertices[ii + 1];
minX = Math.Min(minX, vx);
minY = Math.Min(minY, vy);
maxX = Math.Max(maxX, vx);
maxY = Math.Max(maxY, vy);
}
}
else
{
var boneX = slot.Bone.WorldX;
var boneY = slot.Bone.WorldY;
minX = Math.Min(minX, boneX);
minY = Math.Min(minY, boneY);
maxX = Math.Max(maxX, boneX);
maxY = Math.Max(maxY, boneY);
}
}
x = minX;
y = minY;
w = maxX - minX;
h = maxY - minY;
if (minX >= int.MaxValue || minY >= int.MaxValue || maxX <= int.MinValue || maxY <= int.MinValue)
{
x = self.X;
y = self.Y;
w = 0;
h = 0;
}
else
{
x = minX;
y = minY;
w = maxX - minX;
h = maxY - minY;
}
}
/// <summary>
/// 命中测试, 当插槽全透明或者处于禁用或者骨骼处于未激活则无法命中
/// </summary>
/// <param name="precise">是否精确命中检测, 否则仅使用包围盒进行命中检测</param>
/// <param name="cache">调用方管理的缓存表</param>
public static bool HitTest(this ISlot self, float x, float y, bool precise = false, Dictionary<SFML.Graphics.Texture, SFML.Graphics.Image> cache = null)
public static bool HitTest(this ISlot self, float x, float y)
{
if (self.A <= 0 || !self.Bone.Active || self.Disabled)
return false;
if (!precise)
if (HitTestLevel == HitTestLevel.None || HitTestLevel == HitTestLevel.Bounds)
{
self.GetBounds(out var bx, out var by, out var bw, out var bh);
return x >= bx && x <= (bx + bw) && y >= by && y <= (by + bh);
return x >= bx && x <= bx + bw && y >= by && y <= by + bh;
}
else
else if (HitTestLevel == HitTestLevel.Meshes || HitTestLevel == HitTestLevel.Pixels)
{
float[] vertices = new float[8];
int[] triangles;
@@ -140,22 +186,7 @@ namespace Spine.Interfaces
return false;
}
SFML.Graphics.Image img = null;
if (cache is not null)
{
if (!cache.TryGetValue(tex, out img))
{
img = cache[tex] = tex.CopyToImage();
}
}
else
{
img = tex.CopyToImage();
}
bool hit = false;
var trianglesLength = triangles.Length;
var texSize = img.Size;
for (int i = 0; i + 2 < trianglesLength; i += 3)
{
var idx0 = triangles[i] << 1;
@@ -171,8 +202,11 @@ namespace Spine.Interfaces
float c2 = Cross(x2, y2, x0, y0);
// 判断是否全部同号 (或为 0, 点在边上)
if ((c0 >= 0 && c1 >= 0 && c2 >= 0) || (c0 <= 0 && c1 <= 0 && c2 <= 0))
if (c0 >= 0 && c1 >= 0 && c2 >= 0 || c0 <= 0 && c1 <= 0 && c2 <= 0)
{
if (HitTestLevel == HitTestLevel.Meshes)
return true;
float u0 = uvs[idx0], v0 = uvs[idx0 + 1];
float u1 = uvs[idx1], v1 = uvs[idx1 + 1];
float u2 = uvs[idx2], v2 = uvs[idx2 + 1];
@@ -182,44 +216,62 @@ namespace Spine.Interfaces
float w2 = c0 * inv;
float u = u0 * w0 + u1 * w1 + u2 * w2;
float v = v0 * w0 + v1 * w1 + v2 * w2;
var texW = tex.Size.X;
var texH = tex.Size.Y;
var pixel = img.GetPixel((uint)(u * texSize.X), (uint)(v * texSize.Y));
hit = pixel.A > 0;
break;
// 把要判断的那个像素点渲出来
using var view = _renderTex.GetView();
using var vertexArray = new SFML.Graphics.VertexArray(SFML.Graphics.PrimitiveType.Points, 1);
vertexArray[0] = new(view.Center, new SFML.System.Vector2f(u * texW, v * texH));
// XXX: 此处 RenderTexture 不能临时用临时释放, 由于未知原因如果短时间快速创建释放 RenderTexture 可能让程序卡死
_renderTex.SetActive(true);
_renderTex.Clear(SFML.Graphics.Color.Transparent);
_renderTex.Draw(vertexArray, new(tex));
_renderTex.Display();
_renderTex.SetActive(false);
using var img = _renderTex.Texture.CopyToImage();
var pixel = img.GetPixel(0, 0);
return pixel.A > 0;
}
}
// 无缓存需要立即释放资源
if (cache is null)
{
img.Dispose();
}
return hit;
return false;
}
else
{
throw new NotImplementedException(HitTestLevel.ToString());
}
}
/// <summary>
/// 逐插槽的命中测试, 命中后会提前返回结果中止计算
/// </summary>
public static bool HitTest(this ISkeleton self, float x, float y, bool precise = false)
public static bool HitTest(this ISkeleton self, float x, float y)
{
var cache = new Dictionary<SFML.Graphics.Texture, SFML.Graphics.Image>();
bool hit = self.IterDrawOrder().Any(st => st.HitTest(x, y, precise, cache));
foreach (var img in cache.Values) img.Dispose();
return hit;
}
if (HitTestLevel == HitTestLevel.None)
{
self.GetBounds(out var bx, out var by, out var bw, out var bh);
return x >= bx && x <= bx + bw && y >= by && y <= by + bh;
}
/// <summary>
/// 逐插槽的命中测试, 会完整计算所有插槽的命中情况并按顶层至底层的顺序返回命中的插槽
/// /// <param name="precise">是否精确命中检测, 否则仅使用每个插槽的包围盒进行命中检测</param>
/// </summary>
public static ISlot[] HitTestFull(this ISkeleton self, float x, float y, bool precise = false)
{
var cache = new Dictionary<SFML.Graphics.Texture, SFML.Graphics.Image>();
var hitSlots = self.IterDrawOrder().Where(st => st.HitTest(x, y, precise, cache)).Reverse().ToArray();
foreach (var img in cache.Values) img.Dispose();
return hitSlots;
bool hit = false;
string hitSlotName = "";
foreach (var st in self.IterDrawOrder().Reverse())
{
if (st.HitTest(x, y))
{
hit = true;
hitSlotName = st.Name;
break;
}
}
if (hit && LogHitSlots)
{
_logger.Debug("Hit ({0}): [{1}]", self.Name, hitSlotName);
}
return hit;
}
/// <summary>

View File

@@ -628,7 +628,7 @@ namespace Spine
if (DebugRegions)
{
vt.Color = AttachmentLineColor;
foreach (var slot in _skeleton.Slots.Where(s => s.Bone.Active && !s.Disabled))
foreach (var slot in _skeleton.IterDrawOrder().Where(s => s.A > 0 && s.Bone.Active && !s.Disabled))
{
if (slot.Attachment is IRegionAttachment regionAttachment)
{
@@ -660,7 +660,7 @@ namespace Spine
if (DebugMeshes)
{
vt.Color = MeshLineColor;
foreach (var slot in _skeleton.Slots.Where(s => s.Bone.Active && !s.Disabled))
foreach (var slot in _skeleton.IterDrawOrder().Where(s => s.A > 0 && s.Bone.Active && !s.Disabled))
{
if (slot.Attachment is IMeshAttachment meshAttachment)
{
@@ -696,7 +696,7 @@ namespace Spine
if (DebugMeshHulls)
{
vt.Color = AttachmentLineColor;
foreach (var slot in _skeleton.Slots.Where(s => s.Bone.Active && !s.Disabled))
foreach (var slot in _skeleton.IterDrawOrder().Where(s => s.A > 0 && s.Bone.Active && !s.Disabled))
{
if (slot.Attachment is IMeshAttachment meshAttachment)
{
@@ -742,7 +742,7 @@ namespace Spine
if (DebugClippings)
{
vt.Color = ClippingLineColor;
foreach (var slot in _skeleton.Slots.Where(s => s.Bone.Active && !s.Disabled))
foreach (var slot in _skeleton.IterDrawOrder().Where(s => s.A > 0 && s.Bone.Active && !s.Disabled))
{
if (slot.Attachment is IClippingAttachment clippingAttachment)
{
@@ -799,7 +799,7 @@ namespace Spine
if (DebugBones)
{
var width = Math.Max(Math.Abs(_skeleton.ScaleX), Math.Abs(_skeleton.ScaleY));
foreach (var bone in _skeleton.Bones.Where(b => b.Active))
foreach (var bone in _skeleton.IterDrawOrder().Where(s => s.A > 0 && s.Bone.Active && !s.Disabled).Select(st => st.Bone))
{
var boneLength = bone.Length;
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
@@ -815,7 +815,7 @@ namespace Spine
if (DebugBones)
{
var radius = Math.Max(Math.Abs(_skeleton.ScaleX), Math.Abs(_skeleton.ScaleY));
foreach (var bone in _skeleton.Bones.Where(b => b.Active))
foreach (var bone in _skeleton.IterDrawOrder().Where(s => s.A > 0 && s.Bone.Active && !s.Disabled).Select(st => st.Bone))
{
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
}

View File

@@ -8,28 +8,11 @@
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
<ResourceDictionary Source="/Resources/Geometries.xaml"/>
<ResourceDictionary Source="/Resources/Strings/zh.xaml"/>
<ResourceDictionary Source="/Resources/Skins/Light.xaml"/>
<ResourceDictionary Source="/Resources/Theme.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Style x:Key="MyToggleButton" TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource ToggleButtonSwitch}">
<Setter Property="hc:VisualElement.HighlightBrush" Value="{StaticResource DarkSuccessBrush}"/>
</Style>
<Style TargetType="{x:Type ListBox}" BasedOn="{StaticResource ListBoxBaseStyle}">
<Setter Property="SelectionMode" Value="Extended"/>
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="False"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Visible"/>
</Style>
<Style TargetType="{x:Type ListView}" BasedOn="{StaticResource ListViewBaseStyle}">
<Setter Property="SelectionMode" Value="Extended"/>
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="False"/>
<Setter Property="Background" Value="Transparent"/>
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -1,6 +1,7 @@
using Microsoft.Win32;
using NLog;
using SpineViewer.Natives;
using SpineViewer.Resources;
using SpineViewer.ViewModels.MainWindow;
using SpineViewer.Views;
using System.Collections.Frozen;
@@ -12,6 +13,7 @@ using System.IO;
using System.IO.Pipes;
using System.Reflection;
using System.Windows;
using System.Windows.Interop;
namespace SpineViewer
{
@@ -67,8 +69,7 @@ namespace SpineViewer
_instanceMutex = new Mutex(true, MutexName, out var createdNew);
if (!createdNew)
{
ShowExistedInstance();
SendCommandLineArgs();
ConnectAndSendArgs();
Environment.Exit(0); // 不再启动新实例
return;
}
@@ -98,55 +99,27 @@ namespace SpineViewer
LogManager.Configuration = config;
}
private static void ShowExistedInstance()
private static void ConnectAndSendArgs()
{
try
{
// 遍历同名进程
var processes = Process.GetProcessesByName(ProcessName);
foreach (var p in processes)
{
// 跳过当前进程
if (p.Id == Process.GetCurrentProcess().Id)
continue;
IntPtr hWnd = p.MainWindowHandle;
if (hWnd != IntPtr.Zero)
{
// 3. 显示并置顶窗口
if (User32.IsIconic(hWnd))
{
User32.ShowWindow(hWnd, User32.SW_RESTORE);
}
User32.SetForegroundWindow(hWnd);
break; // 找到一个就可以退出
}
}
}
catch
{
// 忽略异常,不影响当前进程退出
}
}
private static void SendCommandLineArgs()
{
var args = Environment.GetCommandLineArgs().Skip(1).ToArray();
if (args.Length <= 0)
return;
_logger.Info("Send command line args to existed instance, \"{0}\"", string.Join(", ", args));
try
{
// 已有实例在运行,把参数通过命名管道发过去
using (var client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out))
{
client.Connect(10000); // 10 秒超时
using (var writer = new StreamWriter(client))
// 只要启动了实例就要进行连接, 10 秒超时
client.Connect(10000);
// 但是只有有参数的时候才发送参数
var args = Environment.GetCommandLineArgs().Skip(1).ToArray();
if (args.Length > 0)
{
foreach (var v in args)
_logger.Info("Send command line args to existed instance, \"{0}\"", string.Join(", ", args));
using (var writer = new StreamWriter(client))
{
writer.WriteLine(v);
foreach (var v in args)
{
writer.WriteLine(v);
}
}
}
}
@@ -162,6 +135,7 @@ namespace SpineViewer
{
var t = new Task(() =>
{
// 防止实例和窗口还没创建好
while (Current is null) Thread.Sleep(10);
while (true)
{
@@ -174,26 +148,48 @@ namespace SpineViewer
}
while (true)
{
using (var server = new NamedPipeServerStream(PipeName, PipeDirection.In))
try
{
server.WaitForConnection();
using (var reader = new StreamReader(server))
using (var server = new NamedPipeServerStream(PipeName, PipeDirection.In))
{
var args = new List<string>();
string? line;
while ((line = reader.ReadLine()) != null)
args.Add(line);
server.WaitForConnection();
if (args.Count > 0)
// 只要收到连接就可以显示窗口了
Current.Dispatcher.Invoke(() =>
{
Current.Dispatcher.Invoke(() =>
var window = (MainWindow)Current.MainWindow;
window.Show();
if (window.WindowState == WindowState.Minimized)
{
var vm = (MainWindowViewModel)((MainWindow)Current.MainWindow).DataContext;
vm.SpineObjectListViewModel.AddSpineObjectFromFileList(args);
});
window.WindowState = WindowState.Normal;
}
window.Activate();
});
using (var reader = new StreamReader(server))
{
var args = new List<string>();
string? line;
while ((line = reader.ReadLine()) != null)
args.Add(line);
if (args.Count > 0)
{
Current.Dispatcher.Invoke(() =>
{
// 尝试加载参数内容
var window = (MainWindow)Current.MainWindow;
var vm = (MainWindowViewModel)window.DataContext;
vm.SpineObjectListViewModel.AddSpineObjectFromFileList(args);
});
}
}
}
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to process arguments, {0}", ex.Message);
}
}
}, default, TaskCreationOptions.LongRunning);
t.Start();
@@ -231,8 +227,8 @@ namespace SpineViewer
}
catch (Exception ex)
{
_logger.Error("Failed to query autorun registry key, {0}", ex.Message);
_logger.Trace(ex.ToString());
_logger.Error("Failed to query autorun registry key, {0}", ex.Message);
return false;
}
}
@@ -259,8 +255,8 @@ namespace SpineViewer
}
catch (Exception ex)
{
_logger.Error("Failed to set autorun registry key, {0}", ex.Message);
_logger.Trace(ex.ToString());
_logger.Error("Failed to set autorun registry key, {0}", ex.Message);
}
}
}
@@ -343,12 +339,36 @@ namespace SpineViewer
}
catch (Exception ex)
{
_logger.Error("Failed to switch language to {0}, {1}", value, ex.Message);
_logger.Trace(ex.ToString());
_logger.Error("Failed to switch language to {0}, {1}", value, ex.Message);
}
}
}
private AppLanguage _language = AppLanguage.ZH;
public AppSkin Skin
{
get => _skin;
set
{
var uri = $"Resources/Skins/{value.ToString().ToLower()}.xaml";
try
{
Resources.MergedDictionaries.Add(new() { Source = new(uri, UriKind.Relative) });
Resources.MergedDictionaries.Add(new() { Source = new("Resources/Theme.xaml", UriKind.Relative) });
var hwnd = new WindowInteropHelper(Current.MainWindow).Handle;
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
_skin = value;
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to switch skin to {0}, {1}", value, ex.Message);
}
}
}
private AppSkin _skin = AppSkin.Light;
}
public enum AppLanguage
@@ -357,4 +377,11 @@ namespace SpineViewer
EN,
JA
}
public enum AppSkin
{
Light,
Dark,
Violet,
}
}

View File

@@ -1,5 +1,4 @@
using Spine;
using Spine.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;

View File

@@ -1,5 +1,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Spine;
using SpineViewer.Services;
using System;
using System.Collections.Generic;
@@ -85,11 +86,14 @@ namespace SpineViewer.Models
[ObservableProperty]
private AppLanguage _appLanguage;
[ObservableProperty]
private AppSkin _appSkin;
[ObservableProperty]
private bool _renderSelectedOnly;
[ObservableProperty]
private bool _usePreciseHitTest;
private HitTestLevel _hitTestLevel;
[ObservableProperty]
private bool _logHitSlots;

View File

@@ -429,19 +429,11 @@ namespace SpineViewer.Models
}
/// <summary>
/// 命中检测, 可选是否使用精确检测, 会有性能损失
/// 命中检测
/// </summary>
public bool HitTest(float x, float y, bool precise = false)
public bool HitTest(float x, float y)
{
lock (_lock) return _spineObject.Skeleton.HitTest(x, y, precise);
}
/// <summary>
/// 完整的命中检测, 会检测所有插槽是否命中并返回命中的插槽名称
/// </summary>
public string[] HitTestFull(float x, float y, bool precise = false)
{
lock (_lock) return _spineObject.Skeleton.HitTestFull(x, y, precise).Select(v => v.Name).ToArray();
lock (_lock) return _spineObject.Skeleton.HitTest(x, y);
}
public SpineObjectConfigModel ObjectConfig

View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
namespace SpineViewer.Natives
{
/// <summary>
/// dwmapi.dll 包装类
/// </summary>
public static class Dwmapi
{
private const uint DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
private const uint DWMWA_CAPTION_COLOR = 35;
private const uint DWMWA_TEXT_COLOR = 36;
private const uint DWMWA_COLOR_DEFAULT = 0xFFFFFFFF;
[DllImport("dwmapi.dll")]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, uint dwAttribute, ref int pvAttribute, int cbAttribute);
[DllImport("dwmapi.dll")]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, uint dwAttribute, ref uint pvAttribute, int cbAttribute);
public static bool SetWindowCaptionColor(IntPtr hwnd, Color color)
{
int c = color.R | (color.G << 8) | (color.B << 16);
return 0 == DwmSetWindowAttribute(hwnd, DWMWA_CAPTION_COLOR, ref c, sizeof(uint));
}
public static bool SetWindowTextColor(IntPtr hwnd, Color color)
{
int c = color.R | (color.G << 8) | (color.B << 16);
return 0 == DwmSetWindowAttribute(hwnd, DWMWA_TEXT_COLOR, ref c, sizeof(uint));
}
public static bool SetWindowDarkMode(IntPtr hwnd, bool darkMode)
{
int b = darkMode ? 1 : 0;
uint c = DWMWA_COLOR_DEFAULT;
DwmSetWindowAttribute(hwnd, DWMWA_CAPTION_COLOR, ref c, sizeof(uint));
DwmSetWindowAttribute(hwnd, DWMWA_TEXT_COLOR, ref c, sizeof(uint));
return 0 == DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, ref b, sizeof(int));
}
}
}

View File

@@ -12,7 +12,102 @@ namespace SpineViewer.Resources
/// </summary>
public static class AppResource
{
public static T Get<T>(string key) => (T)App.Current.FindResource(key);
private static T Get<T>(string key) => (T)App.Current.FindResource(key);
#region Colors
public static Color Color_LightPrimary => Get<Color>("LightPrimaryColor");
public static Color Color_Primary => Get<Color>("PrimaryColor");
public static Color Color_DarkPrimary => Get<Color>("DarkPrimaryColor");
public static Color Color_LightDanger => Get<Color>("LightDangerColor");
public static Color Color_Danger => Get<Color>("DangerColor");
public static Color Color_DarkDanger => Get<Color>("DarkDangerColor");
public static Color Color_LightWarning => Get<Color>("LightWarningColor");
public static Color Color_Warning => Get<Color>("WarningColor");
public static Color Color_DarkWarning => Get<Color>("DarkWarningColor");
public static Color Color_LightInfo => Get<Color>("LightInfoColor");
public static Color Color_Info => Get<Color>("InfoColor");
public static Color Color_DarkInfo => Get<Color>("DarkInfoColor");
public static Color Color_LightSuccess => Get<Color>("LightSuccessColor");
public static Color Color_Success => Get<Color>("SuccessColor");
public static Color Color_DarkSuccess => Get<Color>("DarkSuccessColor");
public static Color Color_PrimaryText => Get<Color>("PrimaryTextColor");
public static Color Color_SecondaryText => Get<Color>("SecondaryTextColor");
public static Color Color_ThirdlyText => Get<Color>("ThirdlyTextColor");
public static Color Color_ReverseText => Get<Color>("ReverseTextColor");
public static Color Color_TextIcon => Get<Color>("TextIconColor");
public static Color Color_Border => Get<Color>("BorderColor");
public static Color Color_SecondaryBorder => Get<Color>("SecondaryBorderColor");
public static Color Color_Background => Get<Color>("BackgroundColor");
public static Color Color_Region => Get<Color>("RegionColor");
public static Color Color_SecondaryRegion => Get<Color>("SecondaryRegionColor");
public static Color Color_ThirdlyRegion => Get<Color>("ThirdlyRegionColor");
public static Color Color_Title => Get<Color>("TitleColor");
public static Color Color_SecondaryTitle => Get<Color>("SecondaryTitleColor");
public static Color Color_Default => Get<Color>("DefaultColor");
public static Color Color_DarkDefault => Get<Color>("DarkDefaultColor");
public static Color Color_Accent => Get<Color>("AccentColor");
public static Color Color_DarkAccent => Get<Color>("DarkAccentColor");
public static Color Color_DarkMask => Get<Color>("DarkMaskColor");
public static Color Color_DarkOpacity => Get<Color>("DarkOpacityColor");
#endregion
#region Brushes
public static SolidColorBrush Brush_LightPrimary => Get<SolidColorBrush>("LightPrimaryBrush");
public static LinearGradientBrush Brush_Primary => Get<LinearGradientBrush>("PrimaryBrush");
public static SolidColorBrush Brush_DarkPrimary => Get<SolidColorBrush>("DarkPrimaryBrush");
public static SolidColorBrush Brush_PrimaryText => Get<SolidColorBrush>("PrimaryTextBrush");
public static SolidColorBrush Brush_SecondaryText => Get<SolidColorBrush>("SecondaryTextBrush");
public static SolidColorBrush Brush_ThirdlyText => Get<SolidColorBrush>("ThirdlyTextBrush");
public static SolidColorBrush Brush_ReverseText => Get<SolidColorBrush>("ReverseTextBrush");
public static SolidColorBrush Brush_TextIcon => Get<SolidColorBrush>("TextIconBrush");
public static SolidColorBrush Brush_Border => Get<SolidColorBrush>("BorderBrush");
public static SolidColorBrush Brush_SecondaryBorder => Get<SolidColorBrush>("SecondaryBorderBrush");
public static SolidColorBrush Brush_Background => Get<SolidColorBrush>("BackgroundBrush");
public static SolidColorBrush Brush_Region => Get<SolidColorBrush>("RegionBrush");
public static SolidColorBrush Brush_SecondaryRegion => Get<SolidColorBrush>("SecondaryRegionBrush");
public static SolidColorBrush Brush_ThirdlyRegion => Get<SolidColorBrush>("ThirdlyRegionBrush");
public static LinearGradientBrush Brush_Title => Get<LinearGradientBrush>("TitleBrush");
public static SolidColorBrush Brush_Default => Get<SolidColorBrush>("DefaultBrush");
public static SolidColorBrush Brush_DarkDefault => Get<SolidColorBrush>("DarkDefaultBrush");
public static SolidColorBrush Brush_LightDanger => Get<SolidColorBrush>("LightDangerBrush");
public static LinearGradientBrush Brush_Danger => Get<LinearGradientBrush>("DangerBrush");
public static SolidColorBrush Brush_DarkDanger => Get<SolidColorBrush>("DarkDangerBrush");
public static SolidColorBrush Brush_LightWarning => Get<SolidColorBrush>("LightWarningBrush");
public static LinearGradientBrush Brush_Warning => Get<LinearGradientBrush>("WarningBrush");
public static SolidColorBrush Brush_DarkWarning => Get<SolidColorBrush>("DarkWarningBrush");
public static SolidColorBrush Brush_LightInfo => Get<SolidColorBrush>("LightInfoBrush");
public static LinearGradientBrush Brush_Info => Get<LinearGradientBrush>("InfoBrush");
public static SolidColorBrush Brush_DarkInfo => Get<SolidColorBrush>("DarkInfoBrush");
public static SolidColorBrush Brush_LightSuccess => Get<SolidColorBrush>("LightSuccessBrush");
public static LinearGradientBrush Brush_Success => Get<LinearGradientBrush>("SuccessBrush");
public static SolidColorBrush Brush_DarkSuccess => Get<SolidColorBrush>("DarkSuccessBrush");
public static SolidColorBrush Brush_Accent => Get<SolidColorBrush>("AccentBrush");
public static SolidColorBrush Brush_DarkAccent => Get<SolidColorBrush>("DarkAccentBrush");
public static SolidColorBrush Brush_DarkMask => Get<SolidColorBrush>("DarkMaskBrush");
public static SolidColorBrush Brush_DarkOpacity => Get<SolidColorBrush>("DarkOpacityBrush");
#endregion
#region Strings

View File

@@ -0,0 +1,2 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Source="pack://application:,,,/HandyControl;component/Themes/SkinDark.xaml"/>

View File

@@ -0,0 +1,2 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>

View File

@@ -0,0 +1,2 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Source="pack://application:,,,/HandyControl;component/Themes/SkinViolet.xaml"/>

View File

@@ -120,10 +120,9 @@
<s:String x:Key="Str_PlaySpeed">Playback Speed</s:String>
<s:String x:Key="Str_WallpaperView">Wallpaper View</s:String>
<s:String x:Key="Str_RenderSelectedOnly">Render Selected Only</s:String>
<s:String x:Key="Str_UsePreciseHitTest">Use Precise Hit Testing</s:String>
<s:String x:Key="Str_UsePreciseHitTestTooltip">When enabled, click detection will be performed based on pixel transparency of the model.</s:String>
<s:String x:Key="Str_LogHitSlots">Log Hit Slot Names</s:String>
<s:String x:Key="Str_LogHitSlotsTooltip">When enabled, the log box will output the model and slot information hit by each click operation (will not output when using Ctrl for multi-selection).</s:String>
<s:String x:Key="Str_HitTestLevel">Hit Test Accuracy Level</s:String>
<s:String x:Key="Str_LogHitSlots">Output Hit Test Slot Names</s:String>
<s:String x:Key="Str_LogHitSlotsTooltip">When enabled, the log box will output the model and slot information for each click hit test.</s:String>
<s:String x:Key="Str_ShowAxis">Show Axis</s:String>
<s:String x:Key="Str_BackgroundColor">Background Color</s:String>
<s:String x:Key="Str_BackgroundImagePath">Background Image Path</s:String>
@@ -198,11 +197,13 @@
<s:String x:Key="Str_VideoFormat">Video Format</s:String>
<s:String x:Key="Str_LoopPlay">Loop Play</s:String>
<s:String x:Key="Str_LoopPlayTooltip" xml:space="preserve">[Gif/Webp]&#x0A;Whether the animation loops</s:String>
<s:String x:Key="Str_LoopPlayTooltip" xml:space="preserve">[Gif/Webp/Apng]&#x0A;Whether the animation loops</s:String>
<s:String x:Key="Str_QualityParameter">Quality Parameter</s:String>
<s:String x:Key="Str_QualityParameterTooltip" xml:space="preserve">[Webp]&#x0A;Quality parameter, range 0-100, higher value means better quality</s:String>
<s:String x:Key="Str_LosslessParam">Lossless Compression</s:String>
<s:String x:Key="Str_LosslessParamTooltip" xml:space="preserve">[Webp]&#x0A;Lossless compression, quality parameter will be ignored</s:String>
<s:String x:Key="Str_ApngPred">Predictor Method</s:String>
<s:String x:Key="Str_ApngPredTooltip" xml:space="preserve">[Apng]&#x0A;Pred parameter, value range 0-5, corresponding to different encoding strategies: none, sub, up, avg, paeth, and mixed.&#x0A;It affects encoding time and file size.</s:String>
<s:String x:Key="Str_CrfParameter">CRF Parameter</s:String>
<s:String x:Key="Str_CrfParameterTooltip" xml:space="preserve">[Mp4/Webm/Mkv]&#x0A;CRF parameter, range 0-63, lower value means higher quality</s:String>
<s:String x:Key="Str_ProfileParameter">Profile Parameter</s:String>
@@ -243,6 +244,7 @@
<s:String x:Key="Str_AppPreference">Application Options</s:String>
<s:String x:Key="Str_Language">Language</s:String>
<s:String x:Key="Str_AppSkin">Skin</s:String>
<s:String x:Key="Str_CloseToTray">Minimize to tray when closing</s:String>
<s:String x:Key="Str_AutoRun">Auto Start</s:String>
<s:String x:Key="Str_AutoRunWorkspaceConfigPath">Auto-load Workspace File on Startup</s:String>

View File

@@ -120,10 +120,9 @@
<s:String x:Key="Str_PlaySpeed">再生速度</s:String>
<s:String x:Key="Str_WallpaperView">壁紙表示</s:String>
<s:String x:Key="Str_RenderSelectedOnly">選択のみレンダリング</s:String>
<s:String x:Key="Str_UsePreciseHitTest">精密ヒットテストを使用</s:String>
<s:String x:Key="Str_UsePreciseHitTestTooltip">有効にすると、モデルのピクセル透過度に基づいてクリック判定を行います。</s:String>
<s:String x:Key="Str_LogHitSlots">ヒットしたスロット名を出力</s:String>
<s:String x:Key="Str_LogHitSlotsTooltip">有効にすると、ログボックスに各クリック操作でヒットしたモデルとスロットの情報を出力しますCtrlを押しながら複数選択する場合は出力されません。</s:String>
<s:String x:Key="Str_HitTestLevel">ヒットテスト精度レベル</s:String>
<s:String x:Key="Str_LogHitSlots">ヒットテスト結果のスロット名を出力</s:String>
<s:String x:Key="Str_LogHitSlotsTooltip">有効にすると、ログボックスに各クリック操作で命中したモデルとスロットの情報が出力されます。</s:String>
<s:String x:Key="Str_ShowAxis">座標軸を表示</s:String>
<s:String x:Key="Str_BackgroundColor">背景色</s:String>
<s:String x:Key="Str_BackgroundImagePath">背景画像のパス</s:String>
@@ -198,11 +197,13 @@
<s:String x:Key="Str_VideoFormat">ビデオフォーマット</s:String>
<s:String x:Key="Str_LoopPlay">ループ再生</s:String>
<s:String x:Key="Str_LoopPlayTooltip" xml:space="preserve">[Gif/Webp]&#x0A;アニメーションをループ再生するかどうか</s:String>
<s:String x:Key="Str_LoopPlayTooltip" xml:space="preserve">[Gif/Webp/Apng]&#x0A;アニメーションをループ再生するかどうか</s:String>
<s:String x:Key="Str_QualityParameter">品質パラメータ</s:String>
<s:String x:Key="Str_QualityParameterTooltip" xml:space="preserve">[Webp]&#x0A;品質パラメータ、範囲は0-100。値が高いほど品質が良い</s:String>
<s:String x:Key="Str_LosslessParam">無損失圧縮</s:String>
<s:String x:Key="Str_LosslessParamTooltip" xml:space="preserve">[Webp]&#x0A;無損失圧縮、品質パラメータは無視されます</s:String>
<s:String x:Key="Str_ApngPred">予測器方式</s:String>
<s:String x:Key="Str_ApngPredTooltip" xml:space="preserve">[Apng]&#x0A;Pred パラメータ。値の範囲は 05 で、それぞれ none、sub、up、avg、paeth、mixed の異なるエンコード方式に対応します。&#x0A;エンコード時間とファイルサイズに影響します。</s:String>
<s:String x:Key="Str_CrfParameter">CRF パラメータ</s:String>
<s:String x:Key="Str_CrfParameterTooltip" xml:space="preserve">[Mp4/Webm/Mkv]&#x0A;CRF パラメータ、範囲0-63。値が小さいほど品質が高い</s:String>
<s:String x:Key="Str_ProfileParameter">プロファイルパラメータ</s:String>
@@ -243,6 +244,7 @@
<s:String x:Key="Str_AppPreference">アプリケーションプション</s:String>
<s:String x:Key="Str_Language">言語</s:String>
<s:String x:Key="Str_AppSkin">スキン</s:String>
<s:String x:Key="Str_CloseToTray">閉じるときにトレイに最小化する</s:String>
<s:String x:Key="Str_AutoRun">自動起動</s:String>
<s:String x:Key="Str_AutoRunWorkspaceConfigPath">起動時にワークスペースファイルを自動読み込み</s:String>

View File

@@ -120,10 +120,9 @@
<s:String x:Key="Str_PlaySpeed">播放速度</s:String>
<s:String x:Key="Str_WallpaperView">桌面投影</s:String>
<s:String x:Key="Str_RenderSelectedOnly">仅渲染选中</s:String>
<s:String x:Key="Str_UsePreciseHitTest">使用精确命中检测</s:String>
<s:String x:Key="Str_UsePreciseHitTestTooltip">启用后将会按像素透明度来检测点击操作是否命中了模型</s:String>
<s:String x:Key="Str_LogHitSlots">输出命中的插槽名称</s:String>
<s:String x:Key="Str_LogHitSlotsTooltip">启用后将会在日志框内输出每一次点击操作命中的模型和插槽情况(按下 Ctrl 进行多选时不会输出)</s:String>
<s:String x:Key="Str_HitTestLevel">命中检测准确度等级</s:String>
<s:String x:Key="Str_LogHitSlots">输出命中检测结果的插槽名称</s:String>
<s:String x:Key="Str_LogHitSlotsTooltip">启用后将会在日志框内输出每一次点击操作命中的模型和插槽情况</s:String>
<s:String x:Key="Str_ShowAxis">显示坐标轴</s:String>
<s:String x:Key="Str_BackgroundColor">背景颜色</s:String>
<s:String x:Key="Str_BackgroundImagePath">背景图片路径</s:String>
@@ -198,11 +197,13 @@
<s:String x:Key="Str_VideoFormat">视频格式</s:String>
<s:String x:Key="Str_LoopPlay">循环播放</s:String>
<s:String x:Key="Str_LoopPlayTooltip" xml:space="preserve">[Gif/Webp]&#x0A;动图是否循环播放</s:String>
<s:String x:Key="Str_LoopPlayTooltip" xml:space="preserve">[Gif/Webp/Apng]&#x0A;动图是否循环播放</s:String>
<s:String x:Key="Str_QualityParameter">质量参数</s:String>
<s:String x:Key="Str_QualityParameterTooltip" xml:space="preserve">[Webp]&#x0A;质量参数,取值范围 0-100越高质量越好</s:String>
<s:String x:Key="Str_LosslessParam">无损压缩</s:String>
<s:String x:Key="Str_LosslessParamTooltip" xml:space="preserve">[Webp]&#x0A;无损压缩,会忽略质量参数</s:String>
<s:String x:Key="Str_ApngPred">预测器方法</s:String>
<s:String x:Key="Str_ApngPredTooltip" xml:space="preserve">[Apng]&#x0A;Pred 参数,取值范围 0-5分别对应 none、sub、up、avg、paeth、mixed 几种不同的编码策略,&#x0A;影响编码时间和文件大小</s:String>
<s:String x:Key="Str_CrfParameter">CRF 参数</s:String>
<s:String x:Key="Str_CrfParameterTooltip" xml:space="preserve">[Mp4/Webm/Mkv]&#x0A;CRF 参数,取值范围 0-63越小质量越高</s:String>
<s:String x:Key="Str_ProfileParameter">Profile 参数</s:String>
@@ -243,6 +244,7 @@
<s:String x:Key="Str_AppPreference">应用程序选项</s:String>
<s:String x:Key="Str_Language">语言</s:String>
<s:String x:Key="Str_AppSkin">皮肤</s:String>
<s:String x:Key="Str_CloseToTray">关闭时最小化至托盘图标</s:String>
<s:String x:Key="Str_AutoRun">开机自启</s:String>
<s:String x:Key="Str_AutoRunWorkspaceConfigPath">自启动加载工作区文件</s:String>

View File

@@ -0,0 +1,85 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:o="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
xmlns:utils="clr-namespace:SpineViewer.Utils"
xmlns:hc="https://handyorg.github.io/handycontrol">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
</ResourceDictionary.MergedDictionaries>
<utils:StringFormatMultiValueConverter x:Key="StrFmtCvter"/>
<utils:BackgroundToForegroundConverter x:Key="Bg2FgCvter"/>
<Style x:Key="MyGridSplitterBaseStyle" TargetType="{x:Type GridSplitter}">
<Setter Property="Background" Value="{DynamicResource SecondaryBorderBrush}"/>
<Setter Property="ShowsPreview" Value="False"/>
<Style.Triggers>
<Trigger Property="ResizeDirection" Value="Columns">
<Setter Property="Width" Value="3"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
</Trigger>
<Trigger Property="ResizeDirection" Value="Rows">
<Setter Property="Height" Value="3"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="MyToggleButtonBaseStyle" TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource ToggleButtonSwitch}">
<Setter Property="hc:VisualElement.HighlightBrush" Value="{DynamicResource DarkSuccessBrush}"/>
</Style>
<Style x:Key="MyListBoxBaseStyle" TargetType="{x:Type ListBox}" BasedOn="{StaticResource ListBoxBaseStyle}">
<Setter Property="SelectionMode" Value="Extended"/>
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="False"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Visible"/>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource ListBoxItemBaseStyle}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="0"/>
</Style>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="MyListViewBaseStyle" TargetType="{x:Type ListView}" BasedOn="{StaticResource ListViewBaseStyle}">
<Setter Property="SelectionMode" Value="Extended"/>
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="False"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="ItemContainerStyle" Value="{StaticResource ListViewItemBaseStyle.Small}"/>
</Style>
<Style x:Key="MyGroupBoxBaseStyle" TargetType="{x:Type GroupBox}" BasedOn="{StaticResource GroupBoxTab}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="hc:TitleElement.Background" Value="Transparent"/>
</Style>
<Style x:Key="MyLogRichTextBoxStyle" TargetType="{x:Type RichTextBox}" BasedOn="{StaticResource RichTextBoxBaseStyle}">
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="FontFamily" Value="Consolas"/>
<Setter Property="Block.LineHeight" Value="3"/>
<Setter Property="VerticalScrollBarVisibility" Value="Visible"/>
</Style>
<Style x:Key="MyVerticalScrollViewerBaseStyle" TargetType="{x:Type ScrollViewer}" BasedOn="{StaticResource ScrollViewerNativeBaseStyle}">
<Setter Property="Padding" Value="0"/>
<Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
<Style.Triggers>
<Trigger Property="ComputedVerticalScrollBarVisibility" Value="Visible">
<Setter Property="Padding" Value="0 0 5 0"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type GridSplitter}" BasedOn="{StaticResource MyGridSplitterBaseStyle}"/>
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButtonBaseStyle}"/>
<Style TargetType="{x:Type ListBox}" BasedOn="{StaticResource MyListBoxBaseStyle}"/>
<Style TargetType="{x:Type ListView}" BasedOn="{StaticResource MyListViewBaseStyle}"/>
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource MyGroupBoxBaseStyle}"/>
</ResourceDictionary>

View File

@@ -7,7 +7,7 @@
<TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.16.2</Version>
<Version>0.16.7</Version>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
</PropertyGroup>

View File

@@ -0,0 +1,35 @@
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
namespace SpineViewer.Utils
{
public class BackgroundToForegroundConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var color = Colors.White;
if (value is SolidColorBrush brush)
{
color = brush.Color;
}
else if (value is Color c)
{
color = c;
}
if (color.A < 128)
return Brushes.Black;
// 计算亮度 (使用标准加权公式)
double brightness = (0.299 * color.R + 0.587 * color.G + 0.114 * color.B) / 255.0;
return brightness < 0.5 ? Brushes.White : Brushes.Black;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -66,8 +66,8 @@ namespace SpineViewer.Utils
}
catch (Exception ex)
{
_logger.Error("Failed to read json file {0}, {1}", path, ex.Message);
_logger.Trace(ex.ToString());
_logger.Error("Failed to read json file {0}, {1}", path, ex.Message);
MessagePopupService.Error($"Failed to read json file {path}, {ex.ToString()}");
}
}
@@ -88,8 +88,8 @@ namespace SpineViewer.Utils
}
catch (Exception ex)
{
_logger.Error("Failed to save json file {0}, {1}", path, ex.Message);
_logger.Trace(ex.ToString());
_logger.Error("Failed to save json file {0}, {1}", path, ex.Message);
MessagePopupService.Error($"Failed to save json file {path}, {ex.ToString()}");
return false;
}

View File

@@ -7,6 +7,7 @@ using Spine.Exporters;
using SpineViewer.Extensions;
using SpineViewer.Models;
using SpineViewer.Resources;
using SpineViewer.Services;
using SpineViewer.ViewModels.MainWindow;
using System;
using System.Collections;
@@ -74,6 +75,16 @@ namespace SpineViewer.ViewModels.Exporters
public uint MaxResolution { get => _maxResolution; set => SetProperty(ref _maxResolution, value); }
protected uint _maxResolution = 2048;
public RelayCommand Cmd_SelectOutputDir => _cmd_SelectOutputDir ??= new(() =>
{
if (DialogService.ShowOpenFolderDialog(out var selectedPath))
{
_outputDir = selectedPath;
OnPropertyChanged(nameof(OutputDir));
}
});
protected RelayCommand _cmd_SelectOutputDir;
/// <summary>
/// 使用提供的包围盒设置自动分辨率
/// </summary>

View File

@@ -18,26 +18,65 @@ namespace SpineViewer.ViewModels.Exporters
{
public class FFmpegVideoExporterViewModel(MainWindowViewModel vmMain) : VideoExporterViewModel(vmMain)
{
public ImmutableArray<FFmpegVideoExporter.VideoFormat> VideoFormatOptions { get; } = Enum.GetValues<FFmpegVideoExporter.VideoFormat>().ToImmutableArray();
public static ImmutableArray<FFmpegVideoExporter.VideoFormat> VideoFormatOptions { get; } = Enum.GetValues<FFmpegVideoExporter.VideoFormat>().ToImmutableArray();
public FFmpegVideoExporter.VideoFormat Format { get => _format; set => SetProperty(ref _format, value); }
public FFmpegVideoExporter.VideoFormat Format
{
get => _format;
set
{
if (!SetProperty(ref _format, value))
return;
OnPropertyChanged(nameof(EnableParamLoop));
OnPropertyChanged(nameof(EnableParamQuality));
OnPropertyChanged(nameof(EnableParamLossless));
OnPropertyChanged(nameof(EnableParamApngPred));
OnPropertyChanged(nameof(EnableParamCrf));
OnPropertyChanged(nameof(EnableParamProfile));
}
}
protected FFmpegVideoExporter.VideoFormat _format = FFmpegVideoExporter.VideoFormat.Mp4;
public bool Loop { get => _loop; set => SetProperty(ref _loop, value); }
protected bool _loop = true;
public bool EnableParamLoop =>
_format == FFmpegVideoExporter.VideoFormat.Gif ||
_format == FFmpegVideoExporter.VideoFormat.Webp ||
_format == FFmpegVideoExporter.VideoFormat.Apng;
public int Quality { get => _quality; set => SetProperty(ref _quality, Math.Clamp(value, 0, 100)); }
protected int _quality = 75;
public bool EnableParamQuality =>
_format == FFmpegVideoExporter.VideoFormat.Webp;
public bool Lossless { get => _lossless; set => SetProperty(ref _lossless, value); }
protected bool _lossless = false;
public bool EnableParamLossless =>
_format == FFmpegVideoExporter.VideoFormat.Webp;
public int ApngPred { get => _apngPred; set => SetProperty(ref _apngPred, Math.Clamp(value, 0, 5)); }
protected int _apngPred = 5;
public bool EnableParamApngPred =>
_format == FFmpegVideoExporter.VideoFormat.Apng;
public int Crf { get => _crf; set => SetProperty(ref _crf, Math.Clamp(value, 0, 63)); }
protected int _crf = 23;
public bool EnableParamCrf =>
_format == FFmpegVideoExporter.VideoFormat.Mp4 ||
_format == FFmpegVideoExporter.VideoFormat.Webm ||
_format == FFmpegVideoExporter.VideoFormat.Mkv;
public int Profile { get => _profile; set => SetProperty(ref _profile, Math.Clamp(value, -1, 5)); }
protected int _profile = 5;
public bool EnableParamProfile =>
_format == FFmpegVideoExporter.VideoFormat.Mov;
private string FormatSuffix => $".{_format.ToString().ToLower()}";
protected override void Export(SpineObjectModel[] models)
@@ -63,6 +102,7 @@ namespace SpineViewer.ViewModels.Exporters
Loop = _loop,
Quality = _quality,
Lossless = _lossless,
ApngPred = _apngPred,
Crf = _crf,
Profile = _profile,
};

View File

@@ -20,7 +20,7 @@ namespace SpineViewer.ViewModels.Exporters
{
public class FrameExporterViewModel(MainWindowViewModel vmMain) : BaseExporterViewModel(vmMain)
{
public ImmutableArray<SKEncodedImageFormat> FrameFormatOptions { get; } = Enum.GetValues<SKEncodedImageFormat>().ToImmutableArray();
public static ImmutableArray<SKEncodedImageFormat> FrameFormatOptions { get; } = Enum.GetValues<SKEncodedImageFormat>().ToImmutableArray();
public SKEncodedImageFormat Format { get => _format; set => SetProperty(ref _format, value); }
protected SKEncodedImageFormat _format = SKEncodedImageFormat.Png;

View File

@@ -20,7 +20,6 @@ using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shell;
using Spine.Interfaces;
namespace SpineViewer.ViewModels.MainWindow
{

View File

@@ -2,6 +2,7 @@
using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
using NLog;
using Spine;
using Spine.Implementations;
using SpineViewer.Models;
using SpineViewer.Natives;
@@ -108,8 +109,9 @@ namespace SpineViewer.ViewModels.MainWindow
DebugClippings = DebugClippings,
AppLanguage = AppLanguage,
AppSkin = AppSkin,
RenderSelectedOnly = RenderSelectedOnly,
UsePreciseHitTest = UsePreciseHitTest,
HitTestLevel = HitTestLevel,
LogHitSlots = LogHitSlots,
WallpaperView = WallpaperView,
CloseToTray = CloseToTray,
@@ -139,8 +141,9 @@ namespace SpineViewer.ViewModels.MainWindow
DebugClippings = value.DebugClippings;
AppLanguage = value.AppLanguage;
AppSkin = value.AppSkin;
RenderSelectedOnly = value.RenderSelectedOnly;
UsePreciseHitTest = value.UsePreciseHitTest;
HitTestLevel = value.HitTestLevel;
LogHitSlots = value.LogHitSlots;
WallpaperView = value.WallpaperView;
CloseToTray = value.CloseToTray;
@@ -252,28 +255,38 @@ namespace SpineViewer.ViewModels.MainWindow
public static ImmutableArray<AppLanguage> AppLanguageOptions { get; } = Enum.GetValues<AppLanguage>().ToImmutableArray();
public static ImmutableArray<AppSkin> AppSkinOptions { get; } = Enum.GetValues<AppSkin>().ToImmutableArray();
public static ImmutableArray<HitTestLevel> HitTestLevelOptions { get; } = Enum.GetValues<HitTestLevel>().ToImmutableArray();
public AppLanguage AppLanguage
{
get => ((App)App.Current).Language;
set => SetProperty(((App)App.Current).Language, value, v => ((App)App.Current).Language = v);
}
public AppSkin AppSkin
{
get => ((App)App.Current).Skin;
set => SetProperty(((App)App.Current).Skin, value, v => ((App)App.Current).Skin = v);
}
public bool RenderSelectedOnly
{
get => _vmMain.SFMLRendererViewModel.RenderSelectedOnly;
set => SetProperty(_vmMain.SFMLRendererViewModel.RenderSelectedOnly, value, v => _vmMain.SFMLRendererViewModel.RenderSelectedOnly = v);
}
public bool UsePreciseHitTest
public HitTestLevel HitTestLevel
{
get => _vmMain.SFMLRendererViewModel.UsePreciseHitTest;
set => SetProperty(_vmMain.SFMLRendererViewModel.UsePreciseHitTest, value, v => _vmMain.SFMLRendererViewModel.UsePreciseHitTest = v);
get => SpineExtension.HitTestLevel;
set => SetProperty(SpineExtension.HitTestLevel, value, v => SpineExtension.HitTestLevel = v);
}
public bool LogHitSlots
{
get => _vmMain.SFMLRendererViewModel.LogHitSlots;
set => SetProperty(_vmMain.SFMLRendererViewModel.LogHitSlots, value, v => _vmMain.SFMLRendererViewModel.LogHitSlots = v);
get => SpineExtension.LogHitSlots;
set => SetProperty(SpineExtension.LogHitSlots, value, v => SpineExtension.LogHitSlots = v);
}
public bool WallpaperView

View File

@@ -25,7 +25,7 @@ namespace SpineViewer.ViewModels.MainWindow
{
public class SFMLRendererViewModel : ObservableObject
{
public ImmutableArray<Stretch> StretchOptions { get; } = Enum.GetValues<Stretch>().ToImmutableArray();
public static ImmutableArray<Stretch> StretchOptions { get; } = Enum.GetValues<Stretch>().ToImmutableArray();
/// <summary>
/// 日志器
@@ -47,11 +47,6 @@ namespace SpineViewer.ViewModels.MainWindow
/// </summary>
private readonly SFML.Graphics.VertexArray _selectedBackgroundVertices = new(SFML.Graphics.PrimitiveType.Quads, 4); // XXX: 暂时未使用 Dispose 释放
/// <summary>
/// 预览画面坐标轴颜色
/// </summary>
private static readonly SFML.Graphics.Color _axisColor = new(220, 220, 220);
/// <summary>
/// 坐标轴顶点缓冲区
/// </summary>
@@ -178,10 +173,21 @@ namespace SpineViewer.ViewModels.MainWindow
public Color BackgroundColor
{
get => Color.FromRgb(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B);
set => SetProperty(BackgroundColor, value, v => _backgroundColor = new(value.R, value.G, value.B));
set
{
if (!SetProperty(BackgroundColor, value, v => _backgroundColor = new(value.R, value.G, value.B)))
return;
var b = (0.299 * value.R + 0.587 * value.G + 0.114 * value.B) / 255.0;
_axisColor = b < 0.5 ? SFML.Graphics.Color.White : SFML.Graphics.Color.Black;
}
}
private SFML.Graphics.Color _backgroundColor = new(105, 105, 105);
/// <summary>
/// 预览画面坐标轴颜色
/// </summary>
private SFML.Graphics.Color _axisColor = SFML.Graphics.Color.White;
public string BackgroundImagePath
{
get => _backgroundImagePath;
@@ -250,26 +256,6 @@ namespace SpineViewer.ViewModels.MainWindow
}
private bool _renderSelectedOnly;
/// <summary>
/// 启用精确命中测试
/// </summary>
public bool UsePreciseHitTest
{
get => _usePreciseHitTest;
set => SetProperty(ref _usePreciseHitTest, value);
}
private bool _usePreciseHitTest;
/// <summary>
/// 启用完整的命中测试并在日志中输出命中测试的插槽结果
/// </summary>
public bool LogHitSlots
{
get => _logHitSlots;
set => SetProperty(ref _logHitSlots, value);
}
private bool _logHitSlots;
/// <summary>
/// 启用桌面投影
/// </summary>
@@ -368,22 +354,9 @@ namespace SpineViewer.ViewModels.MainWindow
if (_renderSelectedOnly)
{
bool hit = false;
if (!_logHitSlots)
{
// 只在被选中的对象里判断是否有效命中
hit = _models.Any(m => m.IsSelected && m.HitTest(src.X, src.Y, _usePreciseHitTest));
}
else
{
foreach (var sp in _models.Where(m => m.IsSelected))
{
var slotNames = sp.HitTestFull(src.X, src.Y, _usePreciseHitTest);
if (slotNames.Length <= 0) continue;
hit = true;
_logger.Debug("Model Hit ({0}): [{1}]", sp.Name, string.Join(", ", slotNames));
}
}
// 只在被选中的对象里判断是否有效命中
hit = _models.Any(m => m.IsSelected && m.HitTest(src.X, src.Y));
// 如果没点到被选中的模型, 则不允许拖动
if (!hit) _draggingSrc = null;
@@ -395,40 +368,19 @@ namespace SpineViewer.ViewModels.MainWindow
// 没按 Ctrl 的情况下, 如果命中了已选中对象, 则就算普通命中
bool hit = false;
if (!_logHitSlots)
foreach (var sp in _models.Where(m => m.IsShown))
{
foreach (var sp in _models.Where(m => m.IsShown))
if (!sp.HitTest(src.X, src.Y)) continue;
hit = true;
// 如果点到了没被选中的东西, 则清空原先选中的, 改为只选中这一次点的
if (!sp.IsSelected)
{
if (!sp.HitTest(src.X, src.Y, _usePreciseHitTest)) continue;
hit = true;
// 如果点到了没被选中的东西, 则清空原先选中的, 改为只选中这一次点的
if (!sp.IsSelected)
{
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
}
break;
}
}
else
{
foreach (var sp in _models.Where(m => m.IsShown))
{
var slotNames = sp.HitTestFull(src.X, src.Y, _usePreciseHitTest);
if (slotNames.Length <= 0) continue;
// 如果点到了没被选中的东西, 则清空原先选中的, 改为只选中这一次点的
// 仅判断顶层对象 (首次命中)
if (!hit && !sp.IsSelected)
{
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
}
hit = true;
_logger.Debug("Model Hit ({0}): [{1}]", sp.Name, string.Join(", ", slotNames));
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
}
break;
}
// 如果点了空白的地方, 就清空选中列表
@@ -437,7 +389,7 @@ namespace SpineViewer.ViewModels.MainWindow
else
{
// 按下 Ctrl 的情况就执行多选, 并且点空白处也不会清空选中, 如果点击了本来就是选中的则取消选中
if (_models.FirstOrDefault(m => m.IsShown && m.HitTest(src.X, src.Y, _usePreciseHitTest), null) is SpineObjectModel sp)
if (_models.FirstOrDefault(m => m.IsShown && m.HitTest(src.X, src.Y), null) is SpineObjectModel sp)
{
if (sp.IsSelected)
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Remove, sp));

View File

@@ -111,7 +111,7 @@ namespace SpineViewer.ViewModels.MainWindow
return;
if (!DialogService.ShowOpenFileDialog(out var atlasFileName, AppResource.Str_OpenAtlasFileTitle))
return;
AddSpineObject(skelFileName, atlasFileName);
InsertSpineObject(skelFileName, atlasFileName);
_logger.LogCurrentProcessMemoryUsage();
}
@@ -218,8 +218,8 @@ namespace SpineViewer.ViewModels.MainWindow
}
catch (Exception ex)
{
_logger.Error("Failed to reload spine {0}, {1}", sp.SkelPath, ex.Message);
_logger.Trace(ex.ToString());
_logger.Error("Failed to reload spine {0}, {1}", sp.SkelPath, ex.Message);
}
}
}
@@ -279,8 +279,8 @@ namespace SpineViewer.ViewModels.MainWindow
catch (Exception ex)
{
error++;
_logger.Error("Failed to reload spine {0}, {1}", sp.SkelPath, ex.Message);
_logger.Trace(ex.ToString());
_logger.Error("Failed to reload spine {0}, {1}", sp.SkelPath, ex.Message);
}
}
}
@@ -479,7 +479,7 @@ namespace SpineViewer.ViewModels.MainWindow
}
else if (validPaths.Count > 0)
{
AddSpineObject(validPaths[0]);
InsertSpineObject(validPaths[0]);
_logger.LogCurrentProcessMemoryUsage();
}
}
@@ -506,7 +506,7 @@ namespace SpineViewer.ViewModels.MainWindow
var skelPath = paths[i];
reporter.ProgressText = $"[{i}/{totalCount}] {skelPath}";
if (AddSpineObject(skelPath))
if (InsertSpineObject(skelPath))
success++;
else
error++;
@@ -529,7 +529,7 @@ namespace SpineViewer.ViewModels.MainWindow
/// 安全地在列表头添加一个模型, 发生错误会输出日志
/// </summary>
/// <returns>是否添加成功</returns>
private bool AddSpineObject(string skelPath, string? atlasPath = null)
private bool InsertSpineObject(string skelPath, string? atlasPath = null)
{
try
{
@@ -599,7 +599,7 @@ namespace SpineViewer.ViewModels.MainWindow
}
else if (models.Count > 0)
{
AddSpineObject(models[0]);
InsertSpineObject(models[0]);
_logger.LogCurrentProcessMemoryUsage();
}
}
@@ -620,10 +620,10 @@ namespace SpineViewer.ViewModels.MainWindow
{
if (ct.IsCancellationRequested) break;
var cfg = models[i];
var cfg = models[totalCount - 1 - i];
reporter.ProgressText = $"[{i}/{totalCount}] {cfg}";
if (AddSpineObject(cfg))
if (InsertSpineObject(cfg))
success++;
else
error++;
@@ -653,7 +653,7 @@ namespace SpineViewer.ViewModels.MainWindow
/// 安全地在列表头添加一个模型, 发生错误会输出日志
/// </summary>
/// <returns>是否添加成功</returns>
private bool AddSpineObject(SpineObjectWorkspaceConfigModel cfg)
private bool InsertSpineObject(SpineObjectWorkspaceConfigModel cfg)
{
try
{

View File

@@ -18,7 +18,7 @@ namespace SpineViewer.ViewModels.MainWindow
private readonly ObservableCollection<SlotViewModel> _slots = [];
private readonly ObservableCollection<AnimationTrackViewModel> _animationTracks = [];
public ImmutableArray<ISkeleton.Physics> PhysicsOptions { get; } = Enum.GetValues<ISkeleton.Physics>().ToImmutableArray();
public static ImmutableArray<ISkeleton.Physics> PhysicsOptions { get; } = Enum.GetValues<ISkeleton.Physics>().ToImmutableArray();
public SpineObjectModel[] SelectedObjects
{

View File

@@ -8,6 +8,7 @@
d:DataContext="{d:DesignInstance Type=viewmodels:AboutDialogViewModel}"
mc:Ignorable="d"
Title="{DynamicResource Str_Abount}"
Background="{DynamicResource RegionBrush}"
Height="300"
Width="500"
ShowInTaskbar="False"
@@ -26,25 +27,31 @@
<Setter Property="HorizontalContentAlignment" Value="Left"/>
</Style>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 程序版本 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_ProgremVersion}"/>
<Label Grid.Row="0" Grid.Column="1" Content="{Binding ProgramVersion}"/>
<StackPanel Grid.IsSharedSizeScope="True">
<!-- 程序版本 -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ProgremVersion}"/>
<Label Grid.Column="1" Content="{Binding ProgramVersion}"/>
</Grid>
<!-- 项目地址 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_ProjectUrl}"/>
<Label Grid.Row="1" Grid.Column="1">
<Hyperlink NavigateUri="{Binding ProjectUrl}" Command="{Binding Cmd_OpenProjectUrl}">
<Run Text="{Binding ProjectUrl, Mode=OneWay}"/>
</Hyperlink>
</Label>
<!-- 项目地址 -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ProjectUrl}"/>
<Label Grid.Column="1">
<Hyperlink NavigateUri="{Binding ProjectUrl}" Command="{Binding Cmd_OpenProjectUrl}">
<Run Text="{Binding ProjectUrl, Mode=OneWay}"/>
</Hyperlink>
</Label>
</Grid>
</StackPanel>
</Grid>
</Window>

View File

@@ -1,4 +1,6 @@
using System;
using SpineViewer.Natives;
using SpineViewer.Resources;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -8,6 +10,7 @@ using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
@@ -22,6 +25,14 @@ namespace SpineViewer.Views
public AboutDialog()
{
InitializeComponent();
SourceInitialized += AboutDialog_SourceInitialized;
}
private void AboutDialog_SourceInitialized(object? sender, EventArgs e)
{
var hwnd = new WindowInteropHelper(this).Handle;
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
}
}
}

View File

@@ -8,6 +8,7 @@
d:DataContext="{d:DesignInstance Type=viewmodels:DiagnosticsDialogViewModel}"
mc:Ignorable="d"
Title="{DynamicResource Str_Diagnostics}"
Background="{DynamicResource RegionBrush}"
Height="450"
Width="800"
ShowInTaskbar="False"
@@ -21,7 +22,7 @@
<Button Width="120" Content="{DynamicResource Str_CopyDiagnosticsInfo}" Command="{Binding Cmd_CopyToClipboard}"/>
</Border>
<Border Grid.IsSharedSizeScope="True">
<Border>
<Border.Resources>
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource LabelDefault}">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
@@ -31,8 +32,8 @@
<Setter Property="HorizontalContentAlignment" Value="Left"/>
</Style>
</Border.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="30 10">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Grid.IsSharedSizeScope="True" Margin="30 10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
@@ -134,7 +135,6 @@
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding HandyControlVersion, Mode=OneWay}"/>
</Grid>
</StackPanel>
</ScrollViewer>
</Border>
</DockPanel>

View File

@@ -1,4 +1,6 @@
using System;
using SpineViewer.Natives;
using SpineViewer.Resources;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -8,6 +10,7 @@ using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
@@ -22,6 +25,14 @@ namespace SpineViewer.Views
public DiagnosticsDialog()
{
InitializeComponent();
SourceInitialized += DiagnosticsDialog_SourceInitialized;
}
private void DiagnosticsDialog_SourceInitialized(object? sender, EventArgs e)
{
var hwnd = new WindowInteropHelper(this).Handle;
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
}
}
}

View File

@@ -5,10 +5,12 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:local="clr-namespace:SpineViewer.Views.ExporterDialogs"
xmlns:utils="clr-namespace:SpineViewer.Utils"
xmlns:exporters="clr-namespace:SpineViewer.ViewModels.Exporters"
d:DataContext="{d:DesignInstance Type=exporters:CustomFFmpegExporterViewModel}"
mc:Ignorable="d"
Title="{DynamicResource Str_CustomFFmpegExporterTitle}"
Background="{DynamicResource RegionBrush}"
Width="450"
Height="800"
ShowInTaskbar="False"
@@ -39,173 +41,235 @@
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource ComboBoxBaseStyle}">
<Setter Property="HorizontalContentAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButton}">
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButtonBaseStyle}">
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource GroupBoxTab}">
<Setter Property="BorderBrush" Value="LightGray"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="hc:TitleElement.Background" Value="Transparent"/>
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource MyGroupBoxBaseStyle}">
<Setter Property="Margin" Value="0 5 0 10"/>
</Style>
</Border.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ScrollViewer Style="{StaticResource MyVerticalScrollViewerBaseStyle}">
<StackPanel Grid.IsSharedSizeScope="True">
<GroupBox Header="{DynamicResource Str_ExportBaseArgs}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel>
<!-- 水平分辨率 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_ResolutionX}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<TextBox Grid.Row="0" Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionX, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<Grid IsEnabled="{Binding AutoResolution, Converter={StaticResource Boolean2BooleanReConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ResolutionX}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionX, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
</Grid>
<!-- 垂直分辨率 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_ResolutionY}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<TextBox Grid.Row="1" Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionY, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<Grid IsEnabled="{Binding AutoResolution, Converter={StaticResource Boolean2BooleanReConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ResolutionY}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionY, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
</Grid>
<!-- 是否导出单个 -->
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
<ToggleButton Grid.Row="2" Grid.Column="1" IsChecked="{Binding ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
</Grid>
<!-- 输出文件夹 -->
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
<DockPanel Grid.Row="3" Grid.Column="1">
<Button DockPanel.Dock="Right" Content="..." Click="ButtonSelectOutputDir_Click"/>
<TextBox Text="{Binding OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
</DockPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
<DockPanel Grid.Column="1">
<Button DockPanel.Dock="Right" Content="..." Command="{Binding Cmd_SelectOutputDir}"/>
<TextBox Text="{Binding OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
</DockPanel>
</Grid>
<!-- 背景颜色 -->
<Label Grid.Row="4" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
<DockPanel Grid.Row="4" Grid.Column="1">
<Button DockPanel.Dock="Right" Content="..." Click="ButtonPickColor_Click">
<Button.Background>
<SolidColorBrush Color="{Binding BackgroundColor}"/>
</Button.Background>
</Button>
<TextBox x:Name="_colorTextBox" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
</DockPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
<DockPanel Grid.Column="1">
<Border DockPanel.Dock="Right">
<Popup x:Name="_colorPopup" Placement="Right">
<hc:ColorPicker Confirmed="ColorPicker_Confirmed" Canceled="ColorPicker_Canceled"/>
</Popup>
</Border>
<Border DockPanel.Dock="Right" Background="White" CornerRadius="{DynamicResource DefaultCornerRadius}">
<Button Content="..."
Foreground="{Binding BackgroundColor, Converter={StaticResource Bg2FgCvter}}"
Click="ButtonPickColor_Click">
<Button.Background>
<SolidColorBrush Color="{Binding BackgroundColor}"/>
</Button.Background>
</Button>
</Border>
<TextBox Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
</DockPanel>
</Grid>
<!-- 四周边距 -->
<Label Grid.Row="5" Grid.Column="0" Content="{DynamicResource Str_Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
</Grid>
<!-- 自动分辨率 -->
<Label Grid.Row="6" Grid.Column="0" Content="{DynamicResource Str_AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
<ToggleButton Grid.Row="6" Grid.Column="1" IsChecked="{Binding AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
</Grid>
<!-- 最大分辨率 -->
<Label Grid.Row="7" Grid.Column="0" Content="{DynamicResource Str_MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
<TextBox Grid.Row="7" Grid.Column="1" Text="{Binding MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
</Grid>
<Grid IsEnabled="{Binding AutoResolution}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
</Grid>
</StackPanel>
</GroupBox>
<GroupBox Header="{DynamicResource Str_ExportVideoArgs}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel>
<!-- 导出时长 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
</Grid>
<!-- 导出帧率 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_Fps}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Fps}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_Fps}"/>
<TextBox Grid.Column="1" Text="{Binding Fps}"/>
</Grid>
<!-- 导出速度 -->
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
</Grid>
<!-- 是否保留最后一帧 -->
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
<ToggleButton Grid.Row="3" Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
</Grid>
</StackPanel>
</GroupBox>
<GroupBox Header="{DynamicResource Str_ExportOtherArgs}">
<Grid>
<Grid.Resources>
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource LabelDefault}">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource TextBoxBaseStyle}">
<Setter Property="HorizontalContentAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource ComboBoxBaseStyle}">
<Setter Property="HorizontalContentAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButton}">
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="60"/>
</Grid.RowDefinitions>
<StackPanel>
<!-- 导出格式 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_FFmpegFormat}" ToolTip="{DynamicResource Str_FFmpegFormatTooltip}"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Format}" ToolTip="{DynamicResource Str_FFmpegFormatTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_FFmpegFormat}" ToolTip="{DynamicResource Str_FFmpegFormatTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Format}" ToolTip="{DynamicResource Str_FFmpegFormatTooltip}"/>
</Grid>
<!-- 编码器 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_FFmpegCodec}" ToolTip="{DynamicResource Str_FFmpegCodecTooltip}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Codec}" ToolTip="{DynamicResource Str_FFmpegCodecTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_FFmpegCodec}" ToolTip="{DynamicResource Str_FFmpegCodecTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Codec}" ToolTip="{DynamicResource Str_FFmpegCodecTooltip}"/>
</Grid>
<!-- 像素格式 -->
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_FFmpegPixelFormat}" ToolTip="{DynamicResource Str_FFmpegPixelFormatTooltip}"/>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding PixelFormat}" ToolTip="{DynamicResource Str_FFmpegPixelFormatTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_FFmpegPixelFormat}" ToolTip="{DynamicResource Str_FFmpegPixelFormatTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding PixelFormat}" ToolTip="{DynamicResource Str_FFmpegPixelFormatTooltip}"/>
</Grid>
<!-- 比特率 -->
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_FFmpegBitrate}" ToolTip="{DynamicResource Str_FFmpegBitrateTooltip}"/>
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Bitrate}" ToolTip="{DynamicResource Str_FFmpegBitrateTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_FFmpegBitrate}" ToolTip="{DynamicResource Str_FFmpegBitrateTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Bitrate}" ToolTip="{DynamicResource Str_FFmpegBitrateTooltip}"/>
</Grid>
<!-- 滤镜 -->
<Label Grid.Row="4" Grid.Column="0" Content="{DynamicResource Str_FFmpegFilter}" ToolTip="{DynamicResource Str_FFmpegFilterTooltip}"/>
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding Filter}" ToolTip="{DynamicResource Str_FFmpegFilterTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_FFmpegFilter}" ToolTip="{DynamicResource Str_FFmpegFilterTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Filter}" ToolTip="{DynamicResource Str_FFmpegFilterTooltip}"/>
</Grid>
<!-- 自定义参数 -->
<Label Grid.Row="5" Grid.Column="0"
VerticalAlignment="Top"
Content="{DynamicResource Str_FFmpegCustomArgs}"
ToolTip="{DynamicResource Str_FFmpegCustomArgsTooltip}"/>
<TextBox Grid.Row="5" Grid.Column="1"
HorizontalContentAlignment="Left"
VerticalContentAlignment="Top"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto"
Text="{Binding CustomArgs}"
ToolTip="{DynamicResource Str_FFmpegCustomArgsTooltip}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label VerticalAlignment="Top"
Content="{DynamicResource Str_FFmpegCustomArgs}"
ToolTip="{DynamicResource Str_FFmpegCustomArgsTooltip}"/>
<TextBox Grid.Column="1"
Height="75"
HorizontalContentAlignment="Left"
VerticalContentAlignment="Top"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto"
Text="{Binding CustomArgs}"
ToolTip="{DynamicResource Str_FFmpegCustomArgsTooltip}"/>
</Grid>
</StackPanel>
</GroupBox>
</StackPanel>
</ScrollViewer>

View File

@@ -1,4 +1,6 @@
using SpineViewer.Services;
using SpineViewer.Natives;
using SpineViewer.Resources;
using SpineViewer.Services;
using SpineViewer.ViewModels.Exporters;
using System;
using System.Collections.Generic;
@@ -10,6 +12,7 @@ using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
@@ -24,6 +27,14 @@ namespace SpineViewer.Views.ExporterDialogs
public CustomFFmpegExporterDialog()
{
InitializeComponent();
SourceInitialized += CustomFFmpegExporterDialog_SourceInitialized;
}
private void CustomFFmpegExporterDialog_SourceInitialized(object? sender, EventArgs e)
{
var hwnd = new WindowInteropHelper(this).Handle;
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
}
private void ButtonOK_Click(object sender, RoutedEventArgs e)
@@ -42,18 +53,22 @@ namespace SpineViewer.Views.ExporterDialogs
DialogResult = false;
}
private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e)
{
if (DialogService.ShowOpenFolderDialog(out var selectedPath))
{
var vm = (CustomFFmpegExporterViewModel)DataContext;
vm.OutputDir = selectedPath;
}
}
private void ButtonPickColor_Click(object sender, RoutedEventArgs e)
{
_colorPopup.IsOpen = !_colorPopup.IsOpen;
}
private void ColorPicker_Confirmed(object sender, HandyControl.Data.FunctionEventArgs<Color> e)
{
_colorPopup.IsOpen = false;
var color = e.Info;
var vm = (BaseExporterViewModel)DataContext;
vm.BackgroundColor = color;
}
private void ColorPicker_Canceled(object sender, EventArgs e)
{
_colorPopup.IsOpen = false;
}
}
}

View File

@@ -5,10 +5,12 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:local="clr-namespace:SpineViewer.Views.ExporterDialogs"
xmlns:exporters="clr-namespace:SpineViewer.ViewModels.Exporters"
d:DataContext="{d:DesignInstance Type=exporters:FFmpegVideoExporterViewModel}"
xmlns:utils="clr-namespace:SpineViewer.Utils"
xmlns:vmexp="clr-namespace:SpineViewer.ViewModels.Exporters"
d:DataContext="{d:DesignInstance Type=vmexp:FFmpegVideoExporterViewModel}"
mc:Ignorable="d"
Title="{DynamicResource Str_FFmpegVideoExporterTitle}"
Background="{DynamicResource RegionBrush}"
Width="450"
Height="750"
ShowInTaskbar="False"
@@ -39,150 +41,236 @@
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource ComboBoxBaseStyle}">
<Setter Property="HorizontalContentAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButton}">
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButtonBaseStyle}">
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource GroupBoxTab}">
<Setter Property="BorderBrush" Value="LightGray"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="hc:TitleElement.Background" Value="Transparent"/>
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource MyGroupBoxBaseStyle}">
<Setter Property="Margin" Value="0 5 0 10"/>
</Style>
</Border.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ScrollViewer Style="{StaticResource MyVerticalScrollViewerBaseStyle}">
<StackPanel Grid.IsSharedSizeScope="True">
<GroupBox Header="{DynamicResource Str_ExportBaseArgs}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel>
<!-- 水平分辨率 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_ResolutionX}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<TextBox Grid.Row="0" Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionX, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<Grid IsEnabled="{Binding AutoResolution, Converter={StaticResource Boolean2BooleanReConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ResolutionX}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionX, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
</Grid>
<!-- 垂直分辨率 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_ResolutionY}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<TextBox Grid.Row="1" Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionY, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<Grid IsEnabled="{Binding AutoResolution, Converter={StaticResource Boolean2BooleanReConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ResolutionY}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionY, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
</Grid>
<!-- 是否导出单个 -->
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
<ToggleButton Grid.Row="2" Grid.Column="1" IsChecked="{Binding ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
</Grid>
<!-- 输出文件夹 -->
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
<DockPanel Grid.Row="3" Grid.Column="1">
<Button DockPanel.Dock="Right" Content="..." Click="ButtonSelectOutputDir_Click"/>
<TextBox Text="{Binding OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
</DockPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
<DockPanel Grid.Column="1">
<Button DockPanel.Dock="Right" Content="..." Command="{Binding Cmd_SelectOutputDir}"/>
<TextBox Text="{Binding OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
</DockPanel>
</Grid>
<!-- 背景颜色 -->
<Label Grid.Row="4" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
<DockPanel Grid.Row="4" Grid.Column="1">
<Button DockPanel.Dock="Right" Content="..." Click="ButtonPickColor_Click">
<Button.Background>
<SolidColorBrush Color="{Binding BackgroundColor}"/>
</Button.Background>
</Button>
<TextBox x:Name="_colorTextBox" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
</DockPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
<DockPanel Grid.Column="1">
<Border DockPanel.Dock="Right">
<Popup x:Name="_colorPopup" Placement="Right">
<hc:ColorPicker Confirmed="ColorPicker_Confirmed" Canceled="ColorPicker_Canceled"/>
</Popup>
</Border>
<Border DockPanel.Dock="Right" Background="White" CornerRadius="{DynamicResource DefaultCornerRadius}">
<Button Content="..."
Foreground="{Binding BackgroundColor, Converter={StaticResource Bg2FgCvter}}"
Click="ButtonPickColor_Click">
<Button.Background>
<SolidColorBrush Color="{Binding BackgroundColor}"/>
</Button.Background>
</Button>
</Border>
<TextBox Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
</DockPanel>
</Grid>
<!-- 四周边距 -->
<Label Grid.Row="5" Grid.Column="0" Content="{DynamicResource Str_Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
</Grid>
<!-- 自动分辨率 -->
<Label Grid.Row="6" Grid.Column="0" Content="{DynamicResource Str_AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
<ToggleButton Grid.Row="6" Grid.Column="1" IsChecked="{Binding AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
</Grid>
<!-- 最大分辨率 -->
<Label Grid.Row="7" Grid.Column="0" Content="{DynamicResource Str_MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
<TextBox Grid.Row="7" Grid.Column="1" Text="{Binding MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
</Grid>
<Grid IsEnabled="{Binding AutoResolution}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
</Grid>
</StackPanel>
</GroupBox>
<GroupBox Header="{DynamicResource Str_ExportVideoArgs}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel>
<!-- 导出时长 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
</Grid>
<!-- 导出帧率 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_Fps}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Fps}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_Fps}"/>
<TextBox Grid.Column="1" Text="{Binding Fps}"/>
</Grid>
<!-- 导出速度 -->
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
</Grid>
<!-- 是否保留最后一帧 -->
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
<ToggleButton Grid.Row="3" Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
</Grid>
</StackPanel>
</GroupBox>
<GroupBox Header="{DynamicResource Str_ExportOtherArgs}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel>
<!-- 视频格式 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_VideoFormat}"/>
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{Binding VideoFormatOptions}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_VideoFormat}"/>
<ComboBox Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{x:Static vmexp:FFmpegVideoExporterViewModel.VideoFormatOptions}"/>
</Grid>
<!-- 动图是否循环 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_LoopPlay}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/>
<ToggleButton Grid.Row="1" Grid.Column="1" IsChecked="{Binding Loop}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/>
<Grid Visibility="{Binding EnableParamLoop, Converter={StaticResource Boolean2VisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_LoopPlay}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding Loop}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/>
</Grid>
<!-- 质量参数 -->
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_QualityParameter}" ToolTip="{DynamicResource Str_QualityParameterTooltip}"/>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Quality}" ToolTip="{DynamicResource Str_QualityParameterTooltip}"/>
<Grid Visibility="{Binding EnableParamQuality, Converter={StaticResource Boolean2VisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_QualityParameter}" ToolTip="{DynamicResource Str_QualityParameterTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Quality}" ToolTip="{DynamicResource Str_QualityParameterTooltip}"/>
</Grid>
<!-- 无损压缩 -->
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_LosslessParam}" ToolTip="{DynamicResource Str_LosslessParamTooltip}"/>
<ToggleButton Grid.Row="3" Grid.Column="1" IsChecked="{Binding Lossless}" ToolTip="{DynamicResource Str_LosslessParamTooltip}"/>
<Grid Visibility="{Binding EnableParamLossless, Converter={StaticResource Boolean2VisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_LosslessParam}" ToolTip="{DynamicResource Str_LosslessParamTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding Lossless}" ToolTip="{DynamicResource Str_LosslessParamTooltip}"/>
</Grid>
<!-- 预测器方法 -->
<Grid Visibility="{Binding EnableParamApngPred, Converter={StaticResource Boolean2VisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ApngPred}" ToolTip="{DynamicResource Str_ApngPredTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding ApngPred}" ToolTip="{DynamicResource Str_ApngPredTooltip}"/>
</Grid>
<!-- CRF 参数 -->
<Label Grid.Row="4" Grid.Column="0" Content="{DynamicResource Str_CrfParameter}" ToolTip="{DynamicResource Str_CrfParameterTooltip}"/>
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding Crf}" ToolTip="{DynamicResource Str_CrfParameterTooltip}"/>
<Grid Visibility="{Binding EnableParamCrf, Converter={StaticResource Boolean2VisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_CrfParameter}" ToolTip="{DynamicResource Str_CrfParameterTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Crf}" ToolTip="{DynamicResource Str_CrfParameterTooltip}"/>
</Grid>
<!-- Profile 参数 -->
<Label Grid.Row="5" Grid.Column="0" Content="{DynamicResource Str_ProfileParameter}" ToolTip="{DynamicResource Str_ProfileParameterTooltip}"/>
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding Profile}" ToolTip="{DynamicResource Str_ProfileParameterTooltip}"/>
</Grid>
<Grid Visibility="{Binding EnableParamProfile, Converter={StaticResource Boolean2VisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ProfileParameter}" ToolTip="{DynamicResource Str_ProfileParameterTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Profile}" ToolTip="{DynamicResource Str_ProfileParameterTooltip}"/>
</Grid>
</StackPanel>
</GroupBox>
</StackPanel>
</ScrollViewer>

View File

@@ -1,4 +1,6 @@
using SpineViewer.Services;
using SpineViewer.Natives;
using SpineViewer.Resources;
using SpineViewer.Services;
using SpineViewer.ViewModels.Exporters;
using System;
using System.Collections.Generic;
@@ -10,6 +12,7 @@ using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
@@ -24,6 +27,14 @@ namespace SpineViewer.Views.ExporterDialogs
public FFmpegVideoExporterDialog()
{
InitializeComponent();
SourceInitialized += FFmpegVideoExporterDialog_SourceInitialized;
}
private void FFmpegVideoExporterDialog_SourceInitialized(object? sender, EventArgs e)
{
var hwnd = new WindowInteropHelper(this).Handle;
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
}
private void ButtonOK_Click(object sender, RoutedEventArgs e)
@@ -42,18 +53,22 @@ namespace SpineViewer.Views.ExporterDialogs
DialogResult = false;
}
private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e)
{
if (DialogService.ShowOpenFolderDialog(out var selectedPath))
{
var vm = (FFmpegVideoExporterViewModel)DataContext;
vm.OutputDir = selectedPath;
}
}
private void ButtonPickColor_Click(object sender, RoutedEventArgs e)
{
_colorPopup.IsOpen = !_colorPopup.IsOpen;
}
private void ColorPicker_Confirmed(object sender, HandyControl.Data.FunctionEventArgs<Color> e)
{
_colorPopup.IsOpen = false;
var color = e.Info;
var vm = (BaseExporterViewModel)DataContext;
vm.BackgroundColor = color;
}
private void ColorPicker_Canceled(object sender, EventArgs e)
{
_colorPopup.IsOpen = false;
}
}
}

View File

@@ -5,10 +5,12 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:local="clr-namespace:SpineViewer.Views"
xmlns:utils="clr-namespace:SpineViewer.Utils"
xmlns:vmexp="clr-namespace:SpineViewer.ViewModels.Exporters"
d:DataContext="{d:DesignInstance Type=vmexp:FrameExporterViewModel}"
mc:Ignorable="d"
Title="{DynamicResource Str_FrameExporterTitle}"
Background="{DynamicResource RegionBrush}"
Width="450"
Height="480"
ShowInTaskbar="False"
@@ -39,99 +41,142 @@
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource ComboBoxBaseStyle}">
<Setter Property="HorizontalContentAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButton}">
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButtonBaseStyle}">
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource GroupBoxTab}">
<Setter Property="BorderBrush" Value="LightGray"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="hc:TitleElement.Background" Value="Transparent"/>
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource MyGroupBoxBaseStyle}">
<Setter Property="Margin" Value="0 5 0 10"/>
</Style>
</Border.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ScrollViewer Style="{StaticResource MyVerticalScrollViewerBaseStyle}">
<StackPanel Grid.IsSharedSizeScope="True">
<GroupBox Header="{DynamicResource Str_ExportBaseArgs}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel>
<!-- 水平分辨率 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_ResolutionX}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<TextBox Grid.Row="0" Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionX, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<Grid IsEnabled="{Binding AutoResolution, Converter={StaticResource Boolean2BooleanReConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ResolutionX}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionX, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
</Grid>
<!-- 垂直分辨率 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_ResolutionY}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<TextBox Grid.Row="1" Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionY, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<Grid IsEnabled="{Binding AutoResolution, Converter={StaticResource Boolean2BooleanReConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ResolutionY}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionY, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
</Grid>
<!-- 是否导出单个 -->
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
<ToggleButton Grid.Row="2" Grid.Column="1" IsChecked="{Binding ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
</Grid>
<!-- 输出文件夹 -->
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
<DockPanel Grid.Row="3" Grid.Column="1">
<Button DockPanel.Dock="Right" Content="..." Click="ButtonSelectOutputDir_Click"/>
<TextBox Text="{Binding OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
</DockPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
<DockPanel Grid.Column="1">
<Button DockPanel.Dock="Right" Content="..." Command="{Binding Cmd_SelectOutputDir}"/>
<TextBox Text="{Binding OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
</DockPanel>
</Grid>
<!-- 背景颜色 -->
<Label Grid.Row="4" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
<DockPanel Grid.Row="4" Grid.Column="1">
<Button DockPanel.Dock="Right" Content="..." Click="ButtonPickColor_Click">
<Button.Background>
<SolidColorBrush Color="{Binding BackgroundColor}"/>
</Button.Background>
</Button>
<TextBox x:Name="_colorTextBox" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
</DockPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
<DockPanel Grid.Column="1">
<Border DockPanel.Dock="Right">
<Popup x:Name="_colorPopup" Placement="Right">
<hc:ColorPicker Confirmed="ColorPicker_Confirmed" Canceled="ColorPicker_Canceled"/>
</Popup>
</Border>
<Border DockPanel.Dock="Right" Background="White" CornerRadius="{DynamicResource DefaultCornerRadius}">
<Button Content="..."
Foreground="{Binding BackgroundColor, Converter={StaticResource Bg2FgCvter}}"
Click="ButtonPickColor_Click">
<Button.Background>
<SolidColorBrush Color="{Binding BackgroundColor}"/>
</Button.Background>
</Button>
</Border>
<TextBox Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
</DockPanel>
</Grid>
<!-- 四周边距 -->
<Label Grid.Row="5" Grid.Column="0" Content="{DynamicResource Str_Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
</Grid>
<!-- 自动分辨率 -->
<Label Grid.Row="6" Grid.Column="0" Content="{DynamicResource Str_AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
<ToggleButton Grid.Row="6" Grid.Column="1" IsChecked="{Binding AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
</Grid>
<!-- 最大分辨率 -->
<Label Grid.Row="7" Grid.Column="0" Content="{DynamicResource Str_MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
<TextBox Grid.Row="7" Grid.Column="1" Text="{Binding MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
</Grid>
<Grid IsEnabled="{Binding AutoResolution}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
</Grid>
</StackPanel>
</GroupBox>
<GroupBox Header="{DynamicResource Str_ExportOtherArgs}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel>
<!-- 图像格式 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_ImageFormat}"/>
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{Binding FrameFormatOptions}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ImageFormat}"/>
<ComboBox Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{x:Static vmexp:FrameExporterViewModel.FrameFormatOptions}"/>
</Grid>
<!-- 图像质量 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_ImageQuality}" ToolTip="{DynamicResource Str_ImageQualityTooltip}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Quality}" ToolTip="{DynamicResource Str_ImageQualityTooltip}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ImageQuality}" ToolTip="{DynamicResource Str_ImageQualityTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Quality}" ToolTip="{DynamicResource Str_ImageQualityTooltip}"/>
</Grid>
</StackPanel>
</GroupBox>
</StackPanel>
</ScrollViewer>

View File

@@ -1,4 +1,6 @@
using SpineViewer.Services;
using SpineViewer.Natives;
using SpineViewer.Resources;
using SpineViewer.Services;
using SpineViewer.ViewModels.Exporters;
using System;
using System.Collections.Generic;
@@ -10,6 +12,7 @@ using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
@@ -24,6 +27,14 @@ namespace SpineViewer.Views.ExporterDialogs
public FrameExporterDialog()
{
InitializeComponent();
SourceInitialized += FrameExporterDialog_SourceInitialized;
}
private void FrameExporterDialog_SourceInitialized(object? sender, EventArgs e)
{
var hwnd = new WindowInteropHelper(this).Handle;
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
}
private void ButtonOK_Click(object sender, RoutedEventArgs e)
@@ -42,18 +53,22 @@ namespace SpineViewer.Views.ExporterDialogs
DialogResult = false;
}
private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e)
{
if (DialogService.ShowOpenFolderDialog(out var selectedPath))
{
var vm = (FrameExporterViewModel)DataContext;
vm.OutputDir = selectedPath;
}
}
private void ButtonPickColor_Click(object sender, RoutedEventArgs e)
{
_colorPopup.IsOpen = !_colorPopup.IsOpen;
}
private void ColorPicker_Confirmed(object sender, HandyControl.Data.FunctionEventArgs<Color> e)
{
_colorPopup.IsOpen = false;
var color = e.Info;
var vm = (BaseExporterViewModel)DataContext;
vm.BackgroundColor = color;
}
private void ColorPicker_Canceled(object sender, EventArgs e)
{
_colorPopup.IsOpen = false;
}
}
}

View File

@@ -5,10 +5,12 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:local="clr-namespace:SpineViewer.Views"
xmlns:utils="clr-namespace:SpineViewer.Utils"
xmlns:vmexp="clr-namespace:SpineViewer.ViewModels.Exporters"
d:DataContext="{d:DesignInstance Type=vmexp:FrameSequenceExporterViewModel}"
mc:Ignorable="d"
Title="{DynamicResource Str_FrameSequenceExporterTitle}"
Title="{DynamicResource Str_FrameSequenceExporterTitle}"
Background="{DynamicResource RegionBrush}"
Width="450"
Height="550"
ShowInTaskbar="False"
@@ -39,109 +41,162 @@
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource ComboBoxBaseStyle}">
<Setter Property="HorizontalContentAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButton}">
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButtonBaseStyle}">
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource GroupBoxTab}">
<Setter Property="BorderBrush" Value="LightGray"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="hc:TitleElement.Background" Value="Transparent"/>
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource MyGroupBoxBaseStyle}">
<Setter Property="Margin" Value="0 5 0 10"/>
</Style>
</Border.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ScrollViewer Style="{StaticResource MyVerticalScrollViewerBaseStyle}">
<StackPanel Grid.IsSharedSizeScope="True">
<GroupBox Header="{DynamicResource Str_ExportBaseArgs}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel>
<!-- 水平分辨率 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_ResolutionX}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<TextBox Grid.Row="0" Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionX, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<Grid IsEnabled="{Binding AutoResolution, Converter={StaticResource Boolean2BooleanReConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ResolutionX}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionX, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
</Grid>
<!-- 垂直分辨率 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_ResolutionY}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<TextBox Grid.Row="1" Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionY, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<Grid IsEnabled="{Binding AutoResolution, Converter={StaticResource Boolean2BooleanReConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ResolutionY}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionY, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
</Grid>
<!-- 是否导出单个 -->
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
<ToggleButton Grid.Row="2" Grid.Column="1" IsChecked="{Binding ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
</Grid>
<!-- 输出文件夹 -->
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
<DockPanel Grid.Row="3" Grid.Column="1">
<Button DockPanel.Dock="Right" Content="..." Click="ButtonSelectOutputDir_Click"/>
<TextBox Text="{Binding OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
</DockPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
<DockPanel Grid.Column="1">
<Button DockPanel.Dock="Right" Content="..." Command="{Binding Cmd_SelectOutputDir}"/>
<TextBox Text="{Binding OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
</DockPanel>
</Grid>
<!-- 背景颜色 -->
<Label Grid.Row="4" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
<DockPanel Grid.Row="4" Grid.Column="1">
<Button DockPanel.Dock="Right" Content="..." Click="ButtonPickColor_Click">
<Button.Background>
<SolidColorBrush Color="{Binding BackgroundColor}"/>
</Button.Background>
</Button>
<TextBox x:Name="_colorTextBox" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
</DockPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
<DockPanel Grid.Column="1">
<Border DockPanel.Dock="Right">
<Popup x:Name="_colorPopup" Placement="Right">
<hc:ColorPicker Confirmed="ColorPicker_Confirmed" Canceled="ColorPicker_Canceled"/>
</Popup>
</Border>
<Border DockPanel.Dock="Right" Background="White" CornerRadius="{DynamicResource DefaultCornerRadius}">
<Button Content="..."
Foreground="{Binding BackgroundColor, Converter={StaticResource Bg2FgCvter}}"
Click="ButtonPickColor_Click">
<Button.Background>
<SolidColorBrush Color="{Binding BackgroundColor}"/>
</Button.Background>
</Button>
</Border>
<TextBox Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
</DockPanel>
</Grid>
<!-- 四周边距 -->
<Label Grid.Row="5" Grid.Column="0" Content="{DynamicResource Str_Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
</Grid>
<!-- 自动分辨率 -->
<Label Grid.Row="6" Grid.Column="0" Content="{DynamicResource Str_AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
<ToggleButton Grid.Row="6" Grid.Column="1" IsChecked="{Binding AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
</Grid>
<!-- 最大分辨率 -->
<Label Grid.Row="7" Grid.Column="0" Content="{DynamicResource Str_MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
<TextBox Grid.Row="7" Grid.Column="1" Text="{Binding MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
</Grid>
<Grid IsEnabled="{Binding AutoResolution}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
</Grid>
</StackPanel>
</GroupBox>
<GroupBox Header="{DynamicResource Str_ExportVideoArgs}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel>
<!-- 导出时长 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
</Grid>
<!-- 导出帧率 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_Fps}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Fps}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_Fps}"/>
<TextBox Grid.Column="1" Text="{Binding Fps}"/>
</Grid>
<!-- 导出速度 -->
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
<TextBox Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
</Grid>
<!-- 是否保留最后一帧 -->
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
<ToggleButton Grid.Row="3" Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
</Grid>
</StackPanel>
</GroupBox>
</StackPanel>
</ScrollViewer>

View File

@@ -1,4 +1,6 @@
using SpineViewer.Services;
using SpineViewer.Natives;
using SpineViewer.Resources;
using SpineViewer.Services;
using SpineViewer.ViewModels.Exporters;
using System;
using System.Collections.Generic;
@@ -10,6 +12,7 @@ using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
@@ -24,6 +27,14 @@ namespace SpineViewer.Views.ExporterDialogs
public FrameSequenceExporterDialog()
{
InitializeComponent();
SourceInitialized += FrameSequenceExporterDialog_SourceInitialized;
}
private void FrameSequenceExporterDialog_SourceInitialized(object? sender, EventArgs e)
{
var hwnd = new WindowInteropHelper(this).Handle;
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
}
private void ButtonOK_Click(object sender, RoutedEventArgs e)
@@ -42,18 +53,22 @@ namespace SpineViewer.Views.ExporterDialogs
DialogResult = false;
}
private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e)
{
if (DialogService.ShowOpenFolderDialog(out var selectedPath))
{
var vm = (FrameSequenceExporterViewModel)DataContext;
vm.OutputDir = selectedPath;
}
}
private void ButtonPickColor_Click(object sender, RoutedEventArgs e)
{
_colorPopup.IsOpen = !_colorPopup.IsOpen;
}
private void ColorPicker_Confirmed(object sender, HandyControl.Data.FunctionEventArgs<Color> e)
{
_colorPopup.IsOpen = false;
var color = e.Info;
var vm = (BaseExporterViewModel)DataContext;
vm.BackgroundColor = color;
}
private void ColorPicker_Canceled(object sender, EventArgs e)
{
_colorPopup.IsOpen = false;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,12 +8,14 @@ using SpineViewer.Natives;
using SpineViewer.Resources;
using SpineViewer.Services;
using SpineViewer.Utils;
using SpineViewer.ViewModels.Exporters;
using SpineViewer.ViewModels.MainWindow;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using System.Windows.Controls;
@@ -64,11 +66,11 @@ public partial class MainWindow : Window
// XXX: hc 的 NotifyIcon 的 Text 似乎没法双向绑定
_notifyIcon.Text = _vm.Title;
SourceInitialized += MainWindow_SourceInitialized;
Loaded += MainWindow_Loaded;
ContentRendered += MainWindow_ContentRendered;
Closing += MainWindow_Closing;
Closed += MainWindow_Closed;
_vm.SpineObjectListViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging;
_vm.SFMLRendererViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging;
@@ -95,7 +97,7 @@ public partial class MainWindow : Window
rtbTarget.WordColoringRules.Add(new("[I]", "DimGray", "Empty"));
rtbTarget.WordColoringRules.Add(new("[W]", "DarkOrange", "Empty"));
rtbTarget.WordColoringRules.Add(new("[E]", "Red", "Empty"));
rtbTarget.WordColoringRules.Add(new("[F]", "DarkRed", "Empty"));
rtbTarget.WordColoringRules.Add(new("[F]", "White", "DarkRed"));
LogManager.Configuration.AddTarget(rtbTarget);
LogManager.Configuration.AddRule(LogLevel.Debug, LogLevel.Fatal, rtbTarget);
@@ -177,6 +179,13 @@ public partial class MainWindow : Window
#region MainWindow
private void MainWindow_SourceInitialized(object? sender, EventArgs e)
{
var hwnd = new WindowInteropHelper(this).Handle;
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var vm = _vm.SFMLRendererViewModel;
@@ -253,6 +262,28 @@ public partial class MainWindow : Window
#endregion
#region ColorPicker
private void ButtonPickColor_Click(object sender, RoutedEventArgs e)
{
_colorPopup.IsOpen = !_colorPopup.IsOpen;
}
private void ColorPicker_Confirmed(object sender, HandyControl.Data.FunctionEventArgs<Color> e)
{
_colorPopup.IsOpen = false;
var color = e.Info;
var vm = ((MainWindowViewModel)DataContext).SFMLRendererViewModel;
vm.BackgroundColor = color;
}
private void ColorPicker_Canceled(object sender, EventArgs e)
{
_colorPopup.IsOpen = false;
}
#endregion
#region ViewModel PropertyChanged
private void SFMLRendererViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -705,15 +736,17 @@ public partial class MainWindow : Window
}
#endregion
private void DebugMenuItem_Click(object sender, RoutedEventArgs e)
{
#if DEBUG
var a = _rootGrid.ColumnDefinitions[0].Width;
var b = _rootGrid.ColumnDefinitions[1].Width;
var c = _rootGrid.ColumnDefinitions[2].Width;
Debug.WriteLine(a);
Debug.WriteLine(_rootGrid.ColumnDefinitions[0].Width.IsStar);
_logger.Debug("Debug");
_logger.Info("Info");
_logger.Warn("Warn");
_logger.Error("Error");
_logger.Fatal("Fatal");
return;
#endif
}
}

View File

@@ -10,6 +10,7 @@
d:DataContext="{d:DesignInstance Type=models:PreferenceModel}"
mc:Ignorable="d"
Title="{DynamicResource Str_Preference}"
Background="{DynamicResource RegionBrush}"
Height="650"
Width="600"
ShowInTaskbar="False"
@@ -40,24 +41,22 @@
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource ComboBoxBaseStyle}">
<Setter Property="HorizontalContentAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButton}">
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButtonBaseStyle}">
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource GroupBoxTab}">
<Setter Property="BorderBrush" Value="LightGray"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="hc:TitleElement.Background" Value="Transparent"/>
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource MyGroupBoxBaseStyle}">
<Setter Property="Margin" Value="0 5 0 10"/>
</Style>
</Border.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ScrollViewer Style="{StaticResource MyVerticalScrollViewerBaseStyle}">
<StackPanel Grid.IsSharedSizeScope="True">
<GroupBox Header="{DynamicResource Str_TextureLoadPreference}">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ForcePremul}" ToolTip="{DynamicResource Str_ForcePremulTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding ForcePremul}" ToolTip="{DynamicResource Str_ForcePremulTooltip}"/>
@@ -66,7 +65,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ForceNearest}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding ForceNearest}"/>
@@ -75,7 +74,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ForceMipmap}" ToolTip="{DynamicResource Str_ForceMipmapTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding ForceMipmap}" ToolTip="{DynamicResource Str_ForceMipmapTooltip}"/>
@@ -88,7 +87,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_IsShown}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding IsShown}"/>
@@ -97,7 +96,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_UsePma}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding UsePma}"/>
@@ -106,7 +105,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugTexture}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugTexture}"/>
@@ -115,7 +114,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugBounds}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugBounds}"/>
@@ -124,7 +123,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugBones}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugBones}"/>
@@ -133,7 +132,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugRegions}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugRegions}"/>
@@ -142,7 +141,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugMeshHulls}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugMeshHulls}"/>
@@ -151,7 +150,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugMeshes}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugMeshes}"/>
@@ -160,7 +159,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugClippings}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugClippings}"/>
@@ -169,7 +168,7 @@
<!-- <Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugBoundingBoxes}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugBoundingBoxes}"/>
@@ -178,7 +177,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugPaths}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugPaths}"/>
@@ -187,7 +186,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugPoints}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugPoints}"/>
@@ -201,18 +200,29 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_Language}"/>
<ComboBox Grid.Column="1"
SelectedItem="{Binding AppLanguage}"
ItemsSource="{x:Static vm:PreferenceViewModel.AppLanguageOptions}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_Skin}"/>
<ComboBox Grid.Column="1"
SelectedItem="{Binding AppSkin}"
ItemsSource="{x:Static vm:PreferenceViewModel.AppSkinOptions}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_RenderSelectedOnly}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding RenderSelectedOnly}"/>
@@ -221,16 +231,18 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_UsePreciseHitTest}" ToolTip="{DynamicResource Str_UsePreciseHitTestTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding UsePreciseHitTest}" ToolTip="{DynamicResource Str_UsePreciseHitTestTooltip}"/>
<Label Content="{DynamicResource Str_HitTestLevel}"/>
<ComboBox Grid.Column="1"
SelectedItem="{Binding HitTestLevel}"
ItemsSource="{x:Static vm:PreferenceViewModel.HitTestLevelOptions}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_LogHitSlots}" ToolTip="{DynamicResource Str_LogHitSlotsTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding LogHitSlots}" ToolTip="{DynamicResource Str_LogHitSlotsTooltip}"/>
@@ -239,7 +251,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_WallpaperView}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding WallpaperView}"/>
@@ -248,7 +260,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_CloseToTray}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding CloseToTray}"/>
@@ -257,7 +269,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_AutoRun}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding AutoRun}"/>
@@ -266,7 +278,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_AutoRunWorkspaceConfigPath}"
ToolTip="{DynamicResource Str_AutoRunWorkspaceConfigPathTooltip}"/>
@@ -283,7 +295,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_AssociateFileSuffix}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding AssociateFileSuffix}"/>

View File

@@ -1,4 +1,6 @@
using SpineViewer.Services;
using SpineViewer.Natives;
using SpineViewer.Resources;
using SpineViewer.Services;
using SpineViewer.ViewModels.Exporters;
using System;
using System.Collections.Generic;
@@ -10,6 +12,7 @@ using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
@@ -24,6 +27,14 @@ namespace SpineViewer.Views
public PreferenceDialog()
{
InitializeComponent();
SourceInitialized += PreferenceDialog_SourceInitialized;
}
private void PreferenceDialog_SourceInitialized(object? sender, EventArgs e)
{
var hwnd = new WindowInteropHelper(this).Handle;
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
}
private void ButtonOK_Click(object sender, RoutedEventArgs e)

View File

@@ -9,6 +9,7 @@
d:DataContext="{d:DesignInstance Type=vm:ProgressDialogViewModel}"
mc:Ignorable="d"
Title="{Binding Title}"
Background="{DynamicResource RegionBrush}"
Width="550"
Height="250"
ResizeMode="NoResize"
@@ -21,7 +22,11 @@
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding ProgressText}" Padding="20" LineHeight="25" TextWrapping="Wrap"/>
<TextBlock Text="{Binding ProgressText}"
Foreground="{DynamicResource PrimaryTextBrush}"
Padding="20"
LineHeight="25"
TextWrapping="Wrap"/>
<ProgressBar Grid.Row="1"
Value="{Binding Done}"
Maximum="{Binding Total}"
@@ -29,7 +34,7 @@
VerticalAlignment="Center"
Margin="20 5"/>
<Button Grid.Row="2"
Content="{StaticResource Str_Cancel}"
Content="{DynamicResource Str_Cancel}"
Command="{Binding Cmd_Cancel}"
Width="100"
Margin="15"/>

View File

@@ -1,4 +1,6 @@
using SpineViewer.ViewModels;
using SpineViewer.Natives;
using SpineViewer.Resources;
using SpineViewer.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -25,9 +27,17 @@ namespace SpineViewer.Views
public ProgressDialog()
{
InitializeComponent();
SourceInitialized += ProgressDialog_SourceInitialized;
Loaded += ProgressWindow_Loaded;
}
private void ProgressDialog_SourceInitialized(object? sender, EventArgs e)
{
var hwnd = new WindowInteropHelper(this).Handle;
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
}
private void ProgressWindow_Loaded(object sender, RoutedEventArgs e)
{
var hwnd = new WindowInteropHelper(this).Handle;

View File

@@ -4,7 +4,6 @@ using SFML.Graphics;
using SFML.System;
using Spine;
using Spine.Exporters;
using Spine.Interfaces;
namespace SpineViewerCLI
{