Compare commits

...

53 Commits

Author SHA1 Message Date
ww-rm
a28cb3f424 Merge pull request #102 from ww-rm/dev/wpf
v0.15.17
2025-09-21 10:09:56 +08:00
ww-rm
05bb797a91 update to v0.15.17 2025-09-21 10:09:10 +08:00
ww-rm
eb0029a877 update changelog 2025-09-21 10:08:38 +08:00
ww-rm
ef0bfa85aa 修改图标配色 2025-09-21 10:07:27 +08:00
ww-rm
b5721e30a0 update readme 2025-09-21 01:21:38 +08:00
ww-rm
2c3b076b58 Merge pull request #101 from ww-rm/dev/wpf
v0.15.16
2025-09-21 01:14:57 +08:00
ww-rm
01e12f4524 增加打开单模型功能 2025-09-21 01:13:15 +08:00
ww-rm
a814d3d99a update to v0.15.16 2025-09-21 00:55:36 +08:00
ww-rm
6a4508dceb update changelog 2025-09-21 00:55:12 +08:00
ww-rm
b7d7274a5a 增加文件关联首选项 2025-09-21 00:55:01 +08:00
ww-rm
71359a4328 完善多选打开逻辑 2025-09-21 00:01:35 +08:00
ww-rm
3a3691bcca 增加单实例模式和命令行参数 2025-09-20 23:11:47 +08:00
ww-rm
3d649e36cc 完善画布焦点转移逻辑 2025-09-19 00:56:25 +08:00
ww-rm
a24db3c447 增加右键菜单移除全部模型 2025-09-17 23:51:05 +08:00
ww-rm
699a055707 选中项发生变化时转移焦点至模型列表 2025-09-17 23:34:28 +08:00
ww-rm
1f8ed1c31c 增加自动选中最后导入项 2025-09-17 23:28:48 +08:00
ww-rm
e2fc27663c 修改列表每次添加模型在开头 2025-09-17 20:13:03 +08:00
ww-rm
b3cd0b9349 Merge pull request #99 from ww-rm/dev/wpf
v0.15.15
2025-09-11 23:20:45 +08:00
ww-rm
1c545b8c37 update to v0.15.15 2025-09-11 23:19:24 +08:00
ww-rm
d660dd1c4a update changelog 2025-09-11 23:19:18 +08:00
ww-rm
9c0acf7302 增加报错信息 2025-09-11 23:17:13 +08:00
ww-rm
415df555c7 增加导入后自动选中最后一项 2025-09-08 21:50:11 +08:00
ww-rm
5ef13239da Merge pull request #97 from ww-rm/dev/wpf
v0.15.14
2025-09-08 00:07:36 +08:00
ww-rm
13ef873650 update to v0.15.14 2025-09-08 00:05:58 +08:00
ww-rm
78b9834f6c update changelog 2025-09-08 00:05:34 +08:00
ww-rm
672a695b20 增加上一次状态的保存和恢复 2025-09-08 00:00:49 +08:00
ww-rm
e9951ed79a 增加日志版本号输出 2025-09-05 11:36:52 +08:00
ww-rm
0c16b2f104 Merge pull request #93 from ww-rm/dev/wpf
v0.15.13
2025-09-04 20:09:18 +08:00
ww-rm
7628075420 update to v0.15.13 2025-09-04 20:08:08 +08:00
ww-rm
6f896bdaad update changelog 2025-09-04 20:07:54 +08:00
ww-rm
98930db4b6 增加预览画面首选项 2025-09-04 20:07:35 +08:00
ww-rm
c7493372e9 增加布局存储和还原 2025-09-04 19:27:10 +08:00
ww-rm
707aa7f570 Merge pull request #91 from ww-rm/dev/wpf
v0.15.12
2025-09-03 21:58:18 +08:00
ww-rm
99ff6f9f0a 增加轨道参数保存 2025-09-03 21:57:28 +08:00
ww-rm
be8193e235 Merge pull request #90 from ww-rm/dev/wpf
v0.15.12
2025-09-03 21:43:07 +08:00
ww-rm
21b6dbee4c 增加bug issue模板 2025-09-03 21:41:43 +08:00
ww-rm
f60418fecb update readme 2025-09-03 21:34:51 +08:00
ww-rm
1180c735c8 update to v0.15.12 2025-09-03 21:33:18 +08:00
ww-rm
3d8f6547e0 update changelog 2025-09-03 21:32:42 +08:00
ww-rm
99ec2704fe 增加单个轨道的时间因子和alpha混合 2025-09-03 21:30:31 +08:00
ww-rm
dbd2cef766 完善报错信息 2025-09-02 20:46:48 +08:00
ww-rm
212ecc2ff3 增加单个模型的时间因子参数 2025-09-02 00:32:02 +08:00
ww-rm
7806f9298d 增加半透明选中背景 2025-09-01 23:47:08 +08:00
ww-rm
3bc57a8990 增加最大帧率提示文本 2025-08-30 16:31:43 +08:00
ww-rm
67c9ea9291 移动轨道清除功能至右键菜单 2025-08-30 01:38:22 +08:00
ww-rm
f404acc834 修改默认标签页为模型列表 2025-08-21 23:00:40 +08:00
ww-rm
8e1f586d4f Merge pull request #86 from ww-rm/dev/wpf
fix bug
2025-08-20 22:44:47 +08:00
ww-rm
5dd1b84943 fix bug 2025-08-20 22:42:38 +08:00
ww-rm
ad190d8952 Merge pull request #85 from ww-rm/dev/wpf
v0.15.11
2025-08-20 22:36:14 +08:00
ww-rm
ddb11808a7 update to v0.15.11 2025-08-20 22:33:25 +08:00
ww-rm
9b1e26b2ac udpate changelog 2025-08-20 22:33:02 +08:00
ww-rm
c8e35a9aed 增加mov格式 2025-08-20 22:31:49 +08:00
ww-rm
4786b0434c 修复自定义导出参数构造错误 2025-08-19 18:02:21 +08:00
49 changed files with 1799 additions and 740 deletions

18
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,18 @@
---
name: 问题报告/Bug report
about: 报告可能的程序错误/Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
## 问题描述/Describe the bug
清晰完整的描述问题是什么以及如何发生的。/A clear and concise description of what the bug is.
## 复现方式(可选)/To Reproduce (Optional)
## 截图(可选)/Screenshots (Optional)**
如果有必要,提供报错时的有关截图。/If applicable, add screenshots to help explain your problem.
## 附件(可选)/Attachments (Optional)
请将会**出现问题的文件**以及**日志文件**打包成一个 ZIP 后作为附件贴在 issue 内。/Please compress the problematic files and the log files into a single ZIP archive and attach it to this issue.

View File

@@ -1,5 +1,47 @@
# CHANGELOG
## v0.15.17
- 修改图标配色
## v0.15.16
- 修改模型添加顺序, 每次向顶层添加
- 添加模型后自动选中最近添加的模型S
- 点击预览画面或者选中项发生变化时转移焦点至列表
- 增加移除全部菜单项
- 增加单例模式和命令行文件参数
- 增加文件关联设置
## v0.15.15
- 增加报错信息
- 导入后自动选中最后一项
## v0.15.14
- 将预览画面的首选项移动至上一次状态参数中
- 增加预览画面像素的自动保存和恢复
- 增加日志启动时的版本号输出
## v0.15.13
- 增加程序布局自动存储和还原
- 增加部分预览画面首选项
## v0.15.12
- 增加单个模型和单个轨道的时间因子
- 增加单个轨道的 Alpha 混合参数
- 调整轨道清除命令至右键菜单
- 设置默认标签页为模型
- 完善导入时的报错信息
## v0.15.11
- 修复自定义导出中参数构造错误
- 增加 mov 格式及参数说明
## v0.15.10
- 增加插槽可见性参数, 允许任何情况下对插槽启用和禁用对插槽的渲染

View File

@@ -21,11 +21,14 @@ A simple and user-friendly Spine file viewer and exporter with multi-language su
* Skin and custom slot attachment settings.
* Custom slot visibility settings.
* Debug rendering support.
* View/model/track time scale adjustment.
* Track alpha blending parameter settings.
* Fullscreen preview mode.
* Export to single frame/image sequence/animated GIF/video formats.
* Automatic resolution batch export.
* FFmpeg custom export support.
* Program parameter saving.
* File name extension association.
* ...
### Supported Spine Versions

View File

@@ -21,11 +21,14 @@
- 支持皮肤/自定义插槽附件设置
- 支持自定义插槽可见性
- 支持调试渲染
- 支持画面/模型/轨道时间倍速设置
- 支持设置轨道 Alpha 混合参数
- 支持全屏预览
- 支持单帧/动图/视频文件导出
- 支持自动分辨率批量导出
- 支持 FFmpeg 自定义导出
- 支持程序参数保存
- 支持文件后缀关联
- ......
### Spine 版本支持

View File

@@ -60,8 +60,9 @@ namespace Spine.Exporters
if (!string.IsNullOrEmpty(_codec)) options.WithVideoCodec(_codec);
if (!string.IsNullOrEmpty(_pixelFormat)) options.ForcePixelFormat(_pixelFormat);
if (!string.IsNullOrEmpty(_bitrate)) options.WithCustomArgument($"-b:v {_bitrate}");
if (!string.IsNullOrEmpty(_filter)) options.WithCustomArgument($"-vf unpremultiply=inplace=1, {_customArgs}");
if (!string.IsNullOrEmpty(_filter)) options.WithCustomArgument($"-vf \"unpremultiply=inplace=1, {_filter}\"");
else options.WithCustomArgument("-vf unpremultiply=inplace=1");
if (!string.IsNullOrEmpty(_customArgs)) options.WithCustomArgument($"{_customArgs}");
}
/// <summary>

View File

@@ -31,6 +31,7 @@ namespace Spine.Exporters
Mp4,
Webm,
Mkv,
Mov,
}
/// <summary>
@@ -40,29 +41,35 @@ namespace Spine.Exporters
private VideoFormat _format = VideoFormat.Mp4;
/// <summary>
/// 动图是否循环
/// 动图是否循环 [Gif/Webp]
/// </summary>
public bool Loop { get => _loop; set => _loop = value; }
private bool _loop = true;
/// <summary>
/// 质量
/// 质量 [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
/// CRF [Mp4/Webm/Mkv]
/// </summary>
public int Crf { get => _crf; set => _crf = Math.Clamp(value, 0, 63); }
private int _crf = 23;
/// <summary>
/// prores_ks 编码器的配置等级, -1 是自动, 越高质量越好, 只有 4 及以上才有透明通道 [Mov]
/// </summary>
public int Profile { get => _profile; set => _profile = Math.Clamp(value, -1, 5); }
private int _profile = 5;
/// <summary>
/// 获取的一帧, 结果是预乘的
/// </summary>
@@ -89,6 +96,7 @@ namespace Spine.Exporters
VideoFormat.Mp4 => SetMp4Options,
VideoFormat.Webm => SetWebmOptions,
VideoFormat.Mkv => SetMkvOptions,
VideoFormat.Mov => SetMovOptions,
_ => throw new NotImplementedException(),
};
@@ -110,8 +118,8 @@ namespace Spine.Exporters
{
// Gif 固定使用 256 调色板和 128 透明度阈值
var v = "split [s0][s1]";
var s0 = "[s0] palettegen=reserve_transparent=1:max_colors=256 [p]";
var s1 = "[s1][p] paletteuse=dither=bayer:alpha_threshold=128";
var s0 = "[s0] palettegen=max_colors=256 [p]";
var s1 = "[s1][p] paletteuse=alpha_threshold=128";
var customArgs = $"-vf \"unpremultiply=inplace=1, {v};{s0};{s1}\" -loop {(_loop ? 0 : -1)}";
options.ForceFormat("gif")
.WithCustomArgument(customArgs);
@@ -151,5 +159,13 @@ namespace Spine.Exporters
.WithCustomArgument(customArgs);
}
private void SetMovOptions(FFMpegArgumentOptions options)
{
var customArgs = "-vf unpremultiply=inplace=1";
options.ForceFormat("mov").WithVideoCodec("prores_ks").ForcePixelFormat("yuva444p10le")
.WithFastStart()
.WithCustomArgument($"-profile {_profile}")
.WithCustomArgument(customArgs);
}
}
}

View File

@@ -30,8 +30,15 @@ namespace Spine.Implementations.SpineWrappers.V21
: base(skelPath, atlasPath, textureLoader)
{
// 加载 atlas
try { _atlas = new Atlas(atlasPath, textureLoader); }
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
try
{
_atlas = new Atlas(atlasPath, textureLoader);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
}
try
{
@@ -41,8 +48,9 @@ namespace Spine.Implementations.SpineWrappers.V21
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -52,8 +60,9 @@ namespace Spine.Implementations.SpineWrappers.V21
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -61,7 +70,8 @@ namespace Spine.Implementations.SpineWrappers.V21
catch (Exception ex)
{
_atlas.Dispose();
throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex);
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
}
// 加载动画数据

View File

@@ -30,8 +30,15 @@ namespace Spine.Implementations.SpineWrappers.V34
: base(skelPath, atlasPath, textureLoader)
{
// 加载 atlas
try { _atlas = new Atlas(atlasPath, textureLoader); }
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
try
{
_atlas = new Atlas(atlasPath, textureLoader);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
}
try
{
@@ -41,8 +48,9 @@ namespace Spine.Implementations.SpineWrappers.V34
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -52,8 +60,9 @@ namespace Spine.Implementations.SpineWrappers.V34
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -61,7 +70,8 @@ namespace Spine.Implementations.SpineWrappers.V34
catch (Exception ex)
{
_atlas.Dispose();
throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex);
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
}
// 加载动画数据

View File

@@ -30,8 +30,15 @@ namespace Spine.Implementations.SpineWrappers.V35
: base(skelPath, atlasPath, textureLoader)
{
// 加载 atlas
try { _atlas = new Atlas(atlasPath, textureLoader); }
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
try
{
_atlas = new Atlas(atlasPath, textureLoader);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
}
try
{
@@ -41,8 +48,9 @@ namespace Spine.Implementations.SpineWrappers.V35
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -52,8 +60,9 @@ namespace Spine.Implementations.SpineWrappers.V35
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -61,7 +70,8 @@ namespace Spine.Implementations.SpineWrappers.V35
catch (Exception ex)
{
_atlas.Dispose();
throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex);
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
}
// 加载动画数据

View File

@@ -30,8 +30,15 @@ namespace Spine.Implementations.SpineWrappers.V36
: base(skelPath, atlasPath, textureLoader)
{
// 加载 atlas
try { _atlas = new Atlas(atlasPath, textureLoader); }
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
try
{
_atlas = new Atlas(atlasPath, textureLoader);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
}
try
{
@@ -41,8 +48,9 @@ namespace Spine.Implementations.SpineWrappers.V36
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -52,8 +60,9 @@ namespace Spine.Implementations.SpineWrappers.V36
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -61,7 +70,8 @@ namespace Spine.Implementations.SpineWrappers.V36
catch (Exception ex)
{
_atlas.Dispose();
throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex);
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
}
// 加载动画数据

View File

@@ -30,8 +30,15 @@ namespace Spine.Implementations.SpineWrappers.V37
: base(skelPath, atlasPath, textureLoader)
{
// 加载 atlas
try { _atlas = new Atlas(atlasPath, textureLoader); }
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
try
{
_atlas = new Atlas(atlasPath, textureLoader);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
}
try
{
@@ -41,8 +48,9 @@ namespace Spine.Implementations.SpineWrappers.V37
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -52,8 +60,9 @@ namespace Spine.Implementations.SpineWrappers.V37
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -61,7 +70,8 @@ namespace Spine.Implementations.SpineWrappers.V37
catch (Exception ex)
{
_atlas.Dispose();
throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex);
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
}
// 加载动画数据

View File

@@ -31,8 +31,15 @@ namespace Spine.Implementations.SpineWrappers.V38
: base(skelPath, atlasPath, textureLoader)
{
// 加载 atlas
try { _atlas = new Atlas(atlasPath, textureLoader); }
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
try
{
_atlas = new Atlas(atlasPath, textureLoader);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
}
try
{
@@ -42,8 +49,9 @@ namespace Spine.Implementations.SpineWrappers.V38
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -53,8 +61,9 @@ namespace Spine.Implementations.SpineWrappers.V38
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -62,7 +71,8 @@ namespace Spine.Implementations.SpineWrappers.V38
catch (Exception ex)
{
_atlas.Dispose();
throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex);
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
}
// 加载动画数据

View File

@@ -30,10 +30,16 @@ namespace Spine.Implementations.SpineWrappers.V40
: base(skelPath, atlasPath, textureLoader)
{
// 加载 atlas
try { _atlas = new Atlas(atlasPath, textureLoader); }
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
try
{
_atlas = new Atlas(atlasPath, textureLoader);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
}
// 加载 skel
try
{
if (Utf8Validator.IsUtf8(skelPath))
@@ -42,8 +48,9 @@ namespace Spine.Implementations.SpineWrappers.V40
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -53,8 +60,9 @@ namespace Spine.Implementations.SpineWrappers.V40
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -62,7 +70,8 @@ namespace Spine.Implementations.SpineWrappers.V40
catch (Exception ex)
{
_atlas.Dispose();
throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex);
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
}
// 加载动画数据

View File

@@ -30,10 +30,16 @@ namespace Spine.Implementations.SpineWrappers.V41
: base(skelPath, atlasPath, textureLoader)
{
// 加载 atlas
try { _atlas = new Atlas(atlasPath, textureLoader); }
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
try
{
_atlas = new Atlas(atlasPath, textureLoader);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
}
// 加载 skel
try
{
if (Utf8Validator.IsUtf8(skelPath))
@@ -42,8 +48,9 @@ namespace Spine.Implementations.SpineWrappers.V41
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -53,8 +60,9 @@ namespace Spine.Implementations.SpineWrappers.V41
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -62,7 +70,8 @@ namespace Spine.Implementations.SpineWrappers.V41
catch (Exception ex)
{
_atlas.Dispose();
throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex);
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
}
// 加载动画数据

View File

@@ -30,10 +30,16 @@ namespace Spine.Implementations.SpineWrappers.V42
: base(skelPath, atlasPath, textureLoader)
{
// 加载 atlas
try { _atlas = new Atlas(atlasPath, textureLoader); }
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
try
{
_atlas = new Atlas(atlasPath, textureLoader);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
}
// 加载 skel
try
{
if (Utf8Validator.IsUtf8(skelPath))
@@ -42,8 +48,9 @@ namespace Spine.Implementations.SpineWrappers.V42
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -53,8 +60,9 @@ namespace Spine.Implementations.SpineWrappers.V42
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
@@ -62,7 +70,8 @@ namespace Spine.Implementations.SpineWrappers.V42
catch (Exception ex)
{
_atlas.Dispose();
throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex);
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
}
// 加载动画数据

View File

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

View File

@@ -66,10 +66,10 @@ namespace Spine
}
catch (InvalidOperationException)
{
throw new KeyNotFoundException($"Unrecognized skel suffix '{skelPath}'");
throw new KeyNotFoundException($"Unrecognized skel file suffix");
}
}
else if (!File.Exists(atlasPath)) throw new FileNotFoundException($"{nameof(atlasPath)} not found", skelPath);
else if (!File.Exists(atlasPath)) throw new FileNotFoundException($"{nameof(atlasPath)} not found", atlasPath);
AtlasPath = Path.GetFullPath(atlasPath);
// 自动检测版本, 可能会抛出异常
@@ -105,13 +105,21 @@ namespace Spine
// 依然加载不成功就只能报错
if (_data is null || Version is null)
throw new InvalidDataException($"Failed to load spine by existed versions: '{skelPath}', '{atlasPath}'");
throw new InvalidDataException($"Failed to load spine by existed versions");
}
else
{
// 根据版本实例化对象
Version = version;
_data = SpineObjectData.New(Version, skelPath, atlasPath, textureLoader);
try
{
_data = SpineObjectData.New(Version, skelPath, atlasPath, textureLoader);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load spine with version '{version}'");
}
}
// 创建状态实例
@@ -167,6 +175,7 @@ namespace Spine
// 拷贝渲染设置
UsePma = other.UsePma;
Physics = other.Physics;
_animationState.TimeScale = other._animationState.TimeScale;
// 拷贝皮肤加载情况
_skinLoadStatus = other._skinLoadStatus.ToDictionary();

View File

@@ -5,6 +5,7 @@ using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NLog;
using Spine.SpineWrappers.Attachments;
namespace Spine.SpineWrappers
@@ -17,6 +18,8 @@ namespace Spine.SpineWrappers
ISpineObjectData,
IDisposable
{
protected static readonly Logger _logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 构建版本对象
/// </summary>

View File

@@ -1,10 +1,13 @@
using NLog;
using SpineViewer.Natives;
using SpineViewer.Views;
using System.Collections.Frozen;
using System.Configuration;
using System.Data;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Pipes;
using System.Reflection;
using System.Windows;
@@ -15,15 +18,24 @@ namespace SpineViewer
/// </summary>
public partial class App : Application
{
public static string Version => Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
public const string ProgId = "SpineViewer.skel";
public static readonly string ProcessPath = Environment.ProcessPath;
public static readonly string ProcessDirectory = Path.GetDirectoryName(Environment.ProcessPath);
public static readonly string ProcessName = Process.GetCurrentProcess().ProcessName;
public static readonly string Version = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
private const string MutexName = "SpineViewerInstance";
private const string PipeName = "SpineViewerPipe";
private static readonly Logger _logger;
private static readonly Mutex _instanceMutex;
static App()
{
InitializeLogConfiguration();
_logger = LogManager.GetCurrentClassLogger();
_logger.Info("Application Started");
_logger.Info("Application Started, v{0}", Version);
AppDomain.CurrentDomain.UnhandledException += (s, e) =>
{
@@ -35,6 +47,17 @@ namespace SpineViewer
_logger.Error("Unobserved task exception: {0}", e.Exception.Message);
e.SetObserved();
};
// 单例模式加 IPC 通信
_instanceMutex = new Mutex(true, MutexName, out var createdNew);
if (!createdNew)
{
ShowExistedInstance();
SendCommandLineArgs();
Environment.Exit(0); // 不再启动新实例
return;
}
StartPipeServer();
}
private static void InitializeLogConfiguration()
@@ -50,7 +73,9 @@ namespace SpineViewer
ArchiveNumbering = NLog.Targets.ArchiveNumberingMode.Rolling,
ArchiveAboveSize = 1048576,
MaxArchiveFiles = 5,
Layout = "${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${level:uppercase=true} - ${callsite-filename:includeSourcePath=false}:${callsite-linenumber} - ${message}"
Layout = "${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${level:uppercase=true} - ${processid} - ${callsite-filename:includeSourcePath=false}:${callsite-linenumber} - ${message}",
ConcurrentWrites = true,
KeepFileOpen = false,
};
config.AddTarget(fileTarget);
@@ -58,20 +83,113 @@ namespace SpineViewer
LogManager.Configuration = config;
}
private static void ShowExistedInstance()
{
try
{
// 2. 遍历同名进程
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))
{
foreach (var v in args)
{
writer.WriteLine(v);
}
}
}
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to pass command line args to existed instance, {0}", ex.Message);
}
}
private static void StartPipeServer()
{
var t = new Task(() =>
{
while (Current is null) Thread.Sleep(10);
while (true)
{
var windowCreated = false;
Current.Dispatcher.Invoke(() => windowCreated = Current.MainWindow is MainWindow);
if (windowCreated)
break;
else
Thread.Sleep(100);
}
while (true)
{
using (var server = new NamedPipeServerStream(PipeName, PipeDirection.In))
{
server.WaitForConnection();
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(() => ((MainWindow)Current.MainWindow).OpenFiles(args));
}
}
}
}
}, default, TaskCreationOptions.LongRunning);
t.Start();
}
protected override void OnStartup(StartupEventArgs e)
{
// 正式启动窗口
base.OnStartup(e);
var dict = new ResourceDictionary();
var uiCulture = CultureInfo.CurrentUICulture.Name.ToLowerInvariant();
_logger.Info("Current UI Culture: {0}", uiCulture);
if (uiCulture.StartsWith("zh")) { } // 默认就是中文, 无需操作
else if (uiCulture.StartsWith("ja")) Language = AppLanguage.JA;
else Language = AppLanguage.EN;
Resources.MergedDictionaries.Add(dict);
}
private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)

View File

@@ -21,6 +21,8 @@ namespace SpineViewer.Extensions
foreach (var tr in self.AnimationState.IterTracks().Where(t => t is not null))
{
var t = spineObject.AnimationState.SetAnimation(tr!.TrackIndex, tr.Animation, tr.Loop);
t.TimeScale = tr.TimeScale;
t.Alpha = tr.Alpha;
if (keepTrackTime)
t.TrackTime = tr.TrackTime;
}
@@ -38,7 +40,8 @@ namespace SpineViewer.Extensions
foreach (var e in self.AnimationState.IterTracks())
{
if (e is not null)
self.AnimationState.SetAnimation(e.TrackIndex, e.Animation, e.Loop);
e.TrackTime = 0; // 直接重置时间能保留原本的 TrackEntry
//self.AnimationState.SetAnimation(e.TrackIndex, e.Animation, e.Loop);
}
self.Update(0);
}
@@ -65,7 +68,7 @@ namespace SpineViewer.Extensions
/// <summary>
/// 按给定的帧率获取所有轨道第一个条目动画全时长包围盒大小, 是一个耗时操作, 如果可能的话最好缓存结果
/// </summary>
public static Rect GetAnimationBounds(this SpineObject self, float fps = 10)
public static Rect GetAnimationBounds(this SpineObject self, float fps = 30)
{
using var copy = self.Copy();
var bounds = copy.GetCurrentBounds();

View File

@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
namespace SpineViewer.Models
{
public class LastStateModel
{
#region
public double WindowLeft { get; set; }
public double WindowTop { get; set; }
public double WindowWidth { get; set; }
public double WindowHeight { get; set; }
public WindowState WindowState { get; set; }
public double RootGridCol0Width { get; set; }
public double ModelListRow0Height { get; set; }
public double ExplorerGridRow0Height { get; set; }
public double RightPanelGridRow0Height { get; set; }
#endregion
#region
public uint ResolutionX { get; set; } = 1500;
public uint ResolutionY { get; set; } = 1000;
public uint MaxFps { get; set; } = 30;
public float Speed { get; set; } = 1f;
public bool ShowAxis { get; set; } = true;
public Color BackgroundColor { get; set; } = Color.FromRgb(105, 105, 105);
#endregion
}
}

View File

@@ -76,6 +76,9 @@ namespace SpineViewer.Models
[ObservableProperty]
private bool _renderSelectedOnly;
[ObservableProperty]
private bool _associateFileSuffix;
[ObservableProperty]
private AppLanguage _appLanguage;

View File

@@ -17,6 +17,8 @@ namespace SpineViewer.Models
public string Physics { get; set; } = ISkeleton.Physics.Update.ToString();
public float TimeScale { get; set; } = 1f;
public float Scale { get; set; } = 1f;
public bool FlipX { get; set; }
@@ -33,7 +35,7 @@ namespace SpineViewer.Models
public List<string> DisabledSlots { get; set; } = [];
public List<string?> Animations { get; set; } = [];
public List<TrackConfigModel?> Animations { get; set; } = [];
public bool DebugTexture { get; set; } = true;
@@ -54,5 +56,15 @@ namespace SpineViewer.Models
public bool DebugPoints { get; set; }
public bool DebugClippings { get; set; }
}
public class TrackConfigModel
{
public string AnimationName { get; set; } = "";
public float TimeScale { get; set; } = 1f;
public float Alpha { get; set; } = 1f;
}
}

View File

@@ -89,7 +89,7 @@ namespace SpineViewer.Models
public event EventHandler<SlotAttachmentChangedEventArgs>? SlotAttachmentChanged;
public event EventHandler<AnimationChangedEventArgs>? AnimationChanged;
public event EventHandler<TrackPropertyChangedEventArgs>? TrackPropertyChanged;
public SpineVersion Version => _spineObject.Version;
@@ -129,6 +129,12 @@ namespace SpineViewer.Models
set { lock (_lock) SetProperty(_spineObject.Physics, value, v => _spineObject.Physics = v); }
}
public float TimeScale
{
get { lock (_lock) return _spineObject.AnimationState.TimeScale; }
set { lock (_lock) SetProperty(_spineObject.AnimationState.TimeScale, Math.Clamp(value, 0.01f, 100f), v => _spineObject.AnimationState.TimeScale = v); }
}
/// <summary>
/// 缩放倍数, 绝对值大小, 两个方向大小不一致时返回 -1, 设置时不会影响正负号
/// </summary>
@@ -248,15 +254,59 @@ namespace SpineViewer.Models
public void SetAnimation(int index, string name)
{
bool changed = false;
float lastTimeScale = 1f;
float lastAlpha = 1f;
lock (_lock)
{
if (_spineObject.Data.AnimationsByName.ContainsKey(name))
{
_spineObject.AnimationState.SetAnimation(index, name, true);
// 需要记录之前的轨道属性值并还原
if (_spineObject.AnimationState.GetCurrent(index) is ITrackEntry entry)
{
lastTimeScale = entry.TimeScale;
lastAlpha = entry.Alpha;
}
entry = _spineObject.AnimationState.SetAnimation(index, name, true);
entry.TimeScale = lastTimeScale;
entry.Alpha = lastAlpha;
changed = true;
}
}
if (changed) AnimationChanged?.Invoke(this, new(index, name));
if (changed) TrackPropertyChanged?.Invoke(this, new(index, nameof(TrackPropertyChangedEventArgs.AnimationName)));
}
public float GetTrackTimeScale(int index)
{
lock (_lock) return _spineObject.AnimationState.GetCurrent(index)?.TimeScale ?? 1;
}
public void SetTrackTimeScale(int index, float scale)
{
lock (_lock)
{
if (_spineObject.AnimationState.GetCurrent(index) is ITrackEntry entry)
{
entry.TimeScale = Math.Clamp(scale, 0.01f, 100f);
TrackPropertyChanged?.Invoke(this, new(index, nameof(TrackPropertyChangedEventArgs.TimeScale)));
}
}
}
public float GetTrackAlpha(int index)
{
lock (_lock) return _spineObject.AnimationState.GetCurrent(index)?.Alpha ?? 1;
}
public void SetTrackAlpha(int index, float alpha)
{
lock (_lock)
{
if (_spineObject.AnimationState.GetCurrent(index) is ITrackEntry entry)
{
entry.Alpha = Math.Clamp(alpha, 0f, 1f);
TrackPropertyChanged?.Invoke(this, new(index, nameof(TrackPropertyChangedEventArgs.Alpha)));
}
}
}
public int[] GetTrackIndices()
@@ -277,7 +327,7 @@ namespace SpineViewer.Models
public void ClearTrack(int index)
{
lock (_lock) _spineObject.AnimationState.ClearTrack(index);
AnimationChanged?.Invoke(this, new(index, null));
TrackPropertyChanged?.Invoke(this, new(index, nameof(TrackPropertyChangedEventArgs.AnimationName)));
}
public void ResetAnimationsTime()
@@ -388,6 +438,7 @@ namespace SpineViewer.Models
UsePma = _spineObject.UsePma,
Physics = _spineObject.Physics.ToString(),
TimeScale = _spineObject.AnimationState.TimeScale,
DebugTexture = _spineObject.DebugTexture,
DebugBounds = _spineObject.DebugBounds,
@@ -408,7 +459,22 @@ namespace SpineViewer.Models
config.DisabledSlots = _spineObject.Skeleton.Slots.Where(it => it.Disabled).Select(it => it.Name).ToList();
// XXX: 处理空动画
config.Animations.AddRange(_spineObject.AnimationState.IterTracks().Select(tr => tr?.Animation.Name));
foreach (var tr in _spineObject.AnimationState.IterTracks())
{
if (tr is not null)
{
config.Animations.Add(new()
{
AnimationName = tr.Animation.Name,
TimeScale = tr.TimeScale,
Alpha = tr.Alpha
});
}
else
{
config.Animations.Add(null);
}
}
return config;
}
@@ -427,6 +493,7 @@ namespace SpineViewer.Models
SetProperty(_spineObject.Skeleton.Y, value.Y, v => _spineObject.Skeleton.Y = v, nameof(Y));
SetProperty(_spineObject.UsePma, value.UsePma, v => _spineObject.UsePma = v, nameof(UsePma));
SetProperty(_spineObject.Physics, Enum.Parse<ISkeleton.Physics>(value.Physics ?? "Update", true), v => _spineObject.Physics = v, nameof(Physics));
SetProperty(_spineObject.AnimationState.TimeScale, value.TimeScale, v => _spineObject.AnimationState.TimeScale = v, nameof(TimeScale));
foreach (var name in _spineObject.Data.Skins.Select(v => v.Name).Except(value.LoadedSkins))
if (_spineObject.SetSkinStatus(name, false))
@@ -446,11 +513,15 @@ namespace SpineViewer.Models
// XXX: 处理空动画
_spineObject.AnimationState.ClearTracks();
int trackIndex = 0;
foreach (var name in value.Animations)
foreach (var trConfig in value.Animations)
{
if (!string.IsNullOrEmpty(name))
_spineObject.AnimationState.SetAnimation(trackIndex, name, true);
AnimationChanged?.Invoke(this, new(trackIndex, name));
if (trConfig is not null && !string.IsNullOrEmpty(trConfig.AnimationName))
{
var tr = _spineObject.AnimationState.SetAnimation(trackIndex, trConfig.AnimationName, true);
tr.TimeScale = trConfig.TimeScale;
tr.Alpha = trConfig.Alpha;
TrackPropertyChanged?.Invoke(this, new(trackIndex, nameof(TrackPropertyChangedEventArgs.AnimationName)));
}
trackIndex++;
}
@@ -540,10 +611,23 @@ namespace SpineViewer.Models
public string? AttachmentName { get; } = attachmentName;
}
public class AnimationChangedEventArgs(int trackIndex, string? animationName) : EventArgs
/// <summary>
/// 模型动画轨道属性变化事件参数, 需要检索 <c><see cref="PropertyName"/></c> 来确定发生变化的属性是什么
/// </summary>
/// <param name="trackIndex">发生属性变化的轨道索引</param>
/// <param name="propertyName">使用 <c>nameof</c> 设置发生改变的属性名</param>
public class TrackPropertyChangedEventArgs(int trackIndex, string propertyName) : EventArgs
{
public int TrackIndex { get; } = trackIndex;
public string? AnimationName { get; } = animationName;
/// <summary>
/// 发生变化的属性名, 将会使用 <c>nameof</c> 设置为属性名称字符串
/// </summary>
public string PropertyName { get; } = propertyName;
public string? AnimationName { get; }
public float TimeScale { get; } = 1f;
public float Alpha { get; } = 1f;
}
public class SpineObjectLoadOptions

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace SpineViewer.Natives
{
/// <summary>
/// gdi32.dll 包装类
/// </summary>
public static class Gdi32
{
[DllImport("gdi32.dll", SetLastError = true)]
public static extern nint CreateCompatibleDC(nint hdc);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern bool DeleteDC(nint hdc);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern nint SelectObject(nint hdc, nint hgdiobj);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern bool DeleteObject(nint hObject);
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace SpineViewer.Natives
{
/// <summary>
/// shell32.dll 包装类
/// </summary>
public static class Shell32
{
public const uint SHCNE_ASSOCCHANGED = 0x08000000;
public const uint SHCNF_IDLIST = 0x0000;
[DllImport("shell32.dll")]
public static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
}
}

View File

@@ -10,9 +10,9 @@ using System.Windows;
namespace SpineViewer.Natives
{
/// <summary>
/// Win32 Sdk 包装类
/// user32.dll 包装类
/// </summary>
public static class Win32
public static class User32
{
public const int GWL_STYLE = -16;
public const int WS_SIZEBOX = 0x40000;
@@ -178,17 +178,11 @@ namespace SpineViewer.Natives
[DllImport("user32.dll", SetLastError = true)]
public static extern bool ShowWindow(nint hWnd, int nCmdShow);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern nint CreateCompatibleDC(nint hdc);
[DllImport("user32.dll")]
public static extern bool IsIconic(IntPtr hWnd);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern bool DeleteDC(nint hdc);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern nint SelectObject(nint hdc, nint hgdiobj);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern bool DeleteObject(nint hObject);
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);

View File

@@ -19,6 +19,8 @@ namespace SpineViewer.Resources
public static string Str_GeneratePreviewsTitle => Get<string>("Str_GeneratePreviewsTitle");
public static string Str_DeletePreviewsTitle => Get<string>("Str_DeletePreviewsTitle");
public static string Str_AddSpineObjectsTitle => Get<string>("Str_AddSpineObjectsTitle");
public static string Str_OpenSkelFileTitle => Get<string>("Str_OpenSkelFileTitle");
public static string Str_OpenAtlasFileTitle => Get<string>("Str_OpenAtlasFileTitle");
public static string Str_ReloadSpineObjectsTitle => Get<string>("Str_ReloadSpineObjectsTitle");
public static string Str_CustomFFmpegExporterTitle => Get<string>("Str_CustomFFmpegExporterTitle");

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -37,8 +37,11 @@
<s:String x:Key="Str_Show">Show</s:String>
<s:String x:Key="Str_ListViewStatusBar">{0} items, {1} selected</s:String>
<s:String x:Key="Str_AddSpineObject">Add...</s:String>
<s:String x:Key="Str_RemoveSpineObject">Remove</s:String>
<s:String x:Key="Str_OpenSkelFileTitle">Select Skeleton File (skel)</s:String>
<s:String x:Key="Str_OpenAtlasFileTitle">Select Atlas File (atlas)</s:String>
<s:String x:Key="Str_AddSpineObjectFromClipboard">Add from Clipboard</s:String>
<s:String x:Key="Str_RemoveSpineObject">Remove</s:String>
<s:String x:Key="Str_RemoveAllSpineObject">Remove All</s:String>
<s:String x:Key="Str_Reload">Reload</s:String>
<s:String x:Key="Str_MoveUpSpineObject">Move Up</s:String>
<s:String x:Key="Str_MoveDownSpineObject">Move Down</s:String>
@@ -64,6 +67,8 @@
<s:String x:Key="Str_IsShown">Show</s:String>
<s:String x:Key="Str_UsePma">Premultiply Alpha</s:String>
<s:String x:Key="Str_Physics">Physics</s:String>
<s:String x:Key="Str_TimeScale">Time Scale</s:String>
<s:String x:Key="Str_TimeScaleTootltip">Time scale for a single model, must be positive.</s:String>
<s:String x:Key="Str_Transform">Transform</s:String>
<s:String x:Key="Str_Scale">Scale</s:String>
@@ -84,6 +89,11 @@
<s:String x:Key="Str_Animation">Animation</s:String>
<s:String x:Key="Str_AppendTrack">Add</s:String>
<s:String x:Key="Str_InsertTrack">Insert</s:String>
<s:String x:Key="Str_ClearTrack">Clear</s:String>
<s:String x:Key="Str_TrackTimeScale">Time Scale</s:String>
<s:String x:Key="Str_TrackTimeScaleTooltip">Time scale for a single track, must be positive.</s:String>
<s:String x:Key="Str_TrackAlpha">Alpha Blending</s:String>
<s:String x:Key="Str_TrackAlphaTooltip">Value range: 01. Similar to image blending, controls how animations from higher-index tracks mix into lower-index tracks.</s:String>
<s:String x:Key="Str_Debug">Debug</s:String>
<s:String x:Key="Str_DebugTexture">Texture</s:String>
@@ -106,6 +116,7 @@
<s:String x:Key="Str_Zoom">Zoom</s:String>
<s:String x:Key="Str_Rotation">Rotation (Degrees)</s:String>
<s:String x:Key="Str_MaxFps">Max FPS</s:String>
<s:String x:Key="Str_MaxFpsTooltip">Maximum frame rate of the preview. Set to 0 for no limit.</s:String>
<s:String x:Key="Str_PlaySpeed">Playback Speed</s:String>
<s:String x:Key="Str_RenderSelectedOnly">Render Selected Only</s:String>
<s:String x:Key="Str_ShowAxis">Show Axis</s:String>
@@ -149,6 +160,10 @@
<s:String x:Key="Str_InvalidMaxResolution">Valid max resolution required when using auto resolution</s:String>
<s:String x:Key="Str_FFmpegFormatRequired">FFmpeg export format is required</s:String>
<s:String x:Key="Str_ExportBaseArgs">Base Parameters</s:String>
<s:String x:Key="Str_ExportVideoArgs">Video Parameters</s:String>
<s:String x:Key="Str_ExportOtherArgs">Other Parameters</s:String>
<s:String x:Key="Str_ResolutionTooltip">Screen resolution; adjust related parameters in the render settings panel</s:String>
<s:String x:Key="Str_ExportSingle">Export Single</s:String>
<s:String x:Key="Str_ExportSingleTooltip">When checked, export selected models in a single frame; output folder is required</s:String>
@@ -176,13 +191,15 @@
<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">Loop animation; only effective for GIF/WebP formats</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_QualityParameter">Quality Parameter</s:String>
<s:String x:Key="Str_QualityParameterTooltip">Range 0100; higher is better; only for WebP format</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">Lossless compression. Ignores the quality parameter and only applies to WebP format.</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_CrfParameter">CRF Parameter</s:String>
<s:String x:Key="Str_CrfParameterTooltip">Range 063; lower is higher quality; only for MP4/WebM/MKV formats</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>
<s:String x:Key="Str_ProfileParameterTooltip" xml:space="preserve">[Mov]&#x0A;Profile parameter, integer between -1 and 5,&#x0A;-1 means automatic, higher values indicate higher quality,&#x0A;Alpha channel encoding is only available when value is 4 or higher</s:String>
<s:String x:Key="Str_FFmpegFormat">Export Format</s:String>
<s:String x:Key="Str_FFmpegFormatTooltip">FFmpeg export format (equivalent to "-f"), e.g. "mp4", "webm"</s:String>
@@ -215,7 +232,10 @@
<s:String x:Key="Str_SpineLoadPreference">Model Loading Options</s:String>
<s:String x:Key="Str_RendererPreference">Preview Options</s:String>
<s:String x:Key="Str_AppPreference">Application Options</s:String>
<s:String x:Key="Str_AssociateFileSuffix">Associate File Extension</s:String>
<s:String x:Key="Str_Language">Language</s:String>
</ResourceDictionary>

View File

@@ -37,8 +37,11 @@
<s:String x:Key="Str_Show">表示</s:String>
<s:String x:Key="Str_ListViewStatusBar">全{0}件、選択中{1}件</s:String>
<s:String x:Key="Str_AddSpineObject">追加...</s:String>
<s:String x:Key="Str_RemoveSpineObject">削除</s:String>
<s:String x:Key="Str_OpenSkelFileTitle">スケルトンファイルを選択skel</s:String>
<s:String x:Key="Str_OpenAtlasFileTitle">アトラスファイルを選択atlas</s:String>
<s:String x:Key="Str_AddSpineObjectFromClipboard">クリップボードから追加</s:String>
<s:String x:Key="Str_RemoveSpineObject">削除</s:String>
<s:String x:Key="Str_RemoveAllSpineObject">すべて削除</s:String>
<s:String x:Key="Str_Reload">再読み込み</s:String>
<s:String x:Key="Str_MoveUpSpineObject">上へ移動</s:String>
<s:String x:Key="Str_MoveDownSpineObject">下へ移動</s:String>
@@ -64,6 +67,8 @@
<s:String x:Key="Str_IsShown">表示</s:String>
<s:String x:Key="Str_UsePma">プレマルチプライドアルファ</s:String>
<s:String x:Key="Str_Physics">物理</s:String>
<s:String x:Key="Str_TimeScale">時間スケール</s:String>
<s:String x:Key="Str_TimeScaleTootltip">単一モデルの時間スケール。正の値のみ指定可能です。</s:String>
<s:String x:Key="Str_Transform">変換</s:String>
<s:String x:Key="Str_Scale">スケール</s:String>
@@ -84,6 +89,11 @@
<s:String x:Key="Str_Animation">アニメーション</s:String>
<s:String x:Key="Str_AppendTrack">追加</s:String>
<s:String x:Key="Str_InsertTrack">挿入</s:String>
<s:String x:Key="Str_ClearTrack">削除</s:String>
<s:String x:Key="Str_TrackTimeScale">時間スケール</s:String>
<s:String x:Key="Str_TrackTimeScaleTooltip">単一トラックの時間スケール。正の値のみ指定可能です。</s:String>
<s:String x:Key="Str_TrackAlpha">アルファ合成</s:String>
<s:String x:Key="Str_TrackAlphaTooltip">値の範囲01。画像の合成と同様に、高インデックストラックのアニメーションが低インデックストラックにどの程度混合されるかを制御します。</s:String>
<s:String x:Key="Str_Debug">デバッグ</s:String>
<s:String x:Key="Str_DebugTexture">テクスチャ</s:String>
@@ -106,6 +116,7 @@
<s:String x:Key="Str_Zoom">ズーム</s:String>
<s:String x:Key="Str_Rotation">回転(度)</s:String>
<s:String x:Key="Str_MaxFps">最大FPS</s:String>
<s:String x:Key="Str_MaxFpsTooltip">プレビュー画面の最大フレームレート。0 に設定すると制限なし。</s:String>
<s:String x:Key="Str_PlaySpeed">再生速度</s:String>
<s:String x:Key="Str_RenderSelectedOnly">選択のみレンダリング</s:String>
<s:String x:Key="Str_ShowAxis">座標軸を表示</s:String>
@@ -149,6 +160,10 @@
<s:String x:Key="Str_InvalidMaxResolution">自動解像度使用時は有効な最大解像度を指定する必要があります</s:String>
<s:String x:Key="Str_FFmpegFormatRequired">FFmpegエクスポートフォーマットを指定する必要があります</s:String>
<s:String x:Key="Str_ExportBaseArgs">基本パラメータ</s:String>
<s:String x:Key="Str_ExportVideoArgs">ビデオパラメータ</s:String>
<s:String x:Key="Str_ExportOtherArgs">その他のパラメータ</s:String>
<s:String x:Key="Str_ResolutionTooltip">画面解像度。関連パラメーターは画面パネルで調整してください</s:String>
<s:String x:Key="Str_ExportSingle">単一エクスポート</s:String>
<s:String x:Key="Str_ExportSingleTooltip">チェックすると、選択モデルを同一画面でエクスポートし、出力フォルダーの指定が必要になります</s:String>
@@ -176,13 +191,15 @@
<s:String x:Key="Str_VideoFormat">ビデオフォーマット</s:String>
<s:String x:Key="Str_LoopPlay">ループ再生</s:String>
<s:String x:Key="Str_LoopPlayTooltip">アニメーションをループ再生するか。Gif/Webp形式のみ有効です</s:String>
<s:String x:Key="Str_QualityParameter">品質パラメータ</s:String>
<s:String x:Key="Str_QualityParameterTooltip">品質パラメーター。値の範囲は0-100。値が大きいほど品質が高くなります。Webp形式のみ有効です</s:String>
<s:String x:Key="Str_LosslessParam">可逆圧縮</s:String>
<s:String x:Key="Str_LosslessParamTooltip">可逆圧縮を行います。品質パラメータは無視され、WebP形式にのみ適用されます</s:String>
<s:String x:Key="Str_CrfParameter">CRFパラメータ</s:String>
<s:String x:Key="Str_CrfParameterTooltip">CRFパラメーター。値の範囲0-63。値が小さいほど品質が高くなります。Mp4/Webm/Mkv形式のみ有効です</s:String>
<s:String x:Key="Str_LoopPlayTooltip" xml:space="preserve">[Gif/Webp]&#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_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>
<s:String x:Key="Str_ProfileParameterTooltip" xml:space="preserve">[Mov]&#x0A;プロファイルパラメータ、-1から5の整数、&#x0A;-1は自動、値が大きいほど品質が高い、&#x0A;値が4以上の場合のみアルファチャンネルをエンコード可能</s:String>
<s:String x:Key="Str_FFmpegFormat">エクスポートフォーマット</s:String>
<s:String x:Key="Str_FFmpegFormatTooltip">FFmpegエクスポートフォーマット。パラメーター“-f”に相当します。例: “mp4”、“webm”</s:String>
@@ -215,7 +232,10 @@
<s:String x:Key="Str_SpineLoadPreference">モデル読み込みオプション</s:String>
<s:String x:Key="Str_RendererPreference">プレビュー画面オプション</s:String>
<s:String x:Key="Str_AppPreference">アプリケーションプション</s:String>
<s:String x:Key="Str_AssociateFileSuffix">ファイル拡張子を関連付ける</s:String>
<s:String x:Key="Str_Language">言語</s:String>
</ResourceDictionary>

View File

@@ -37,8 +37,11 @@
<s:String x:Key="Str_Show">显示</s:String>
<s:String x:Key="Str_ListViewStatusBar">共 {0} 项,已选择 {1} 项</s:String>
<s:String x:Key="Str_AddSpineObject">添加...</s:String>
<s:String x:Key="Str_RemoveSpineObject">移除</s:String>
<s:String x:Key="Str_OpenSkelFileTitle">选择骨骼文件skel</s:String>
<s:String x:Key="Str_OpenAtlasFileTitle">选择图集文件atlas</s:String>
<s:String x:Key="Str_AddSpineObjectFromClipboard">从剪贴板添加</s:String>
<s:String x:Key="Str_RemoveSpineObject">移除</s:String>
<s:String x:Key="Str_RemoveAllSpineObject">移除全部</s:String>
<s:String x:Key="Str_Reload">重新加载</s:String>
<s:String x:Key="Str_MoveUpSpineObject">上移</s:String>
<s:String x:Key="Str_MoveDownSpineObject">下移</s:String>
@@ -64,6 +67,8 @@
<s:String x:Key="Str_IsShown">显示</s:String>
<s:String x:Key="Str_UsePma">预乘Alpha通道</s:String>
<s:String x:Key="Str_Physics">物理</s:String>
<s:String x:Key="Str_TimeScale">时间因子</s:String>
<s:String x:Key="Str_TimeScaleTootltip">单个模型的时间因子,只能取正数</s:String>
<s:String x:Key="Str_Transform">变换</s:String>
<s:String x:Key="Str_Scale">缩放</s:String>
@@ -84,6 +89,11 @@
<s:String x:Key="Str_Animation">动画</s:String>
<s:String x:Key="Str_AppendTrack">添加</s:String>
<s:String x:Key="Str_InsertTrack">插入</s:String>
<s:String x:Key="Str_ClearTrack">删除</s:String>
<s:String x:Key="Str_TrackTimeScale">时间因子</s:String>
<s:String x:Key="Str_TrackTimeScaleTooltip">单个轨道的时间因子,只能取正数</s:String>
<s:String x:Key="Str_TrackAlpha">Alpha 混合</s:String>
<s:String x:Key="Str_TrackAlphaTooltip">取值范围 0-1与图像混合类似可以控制高索引轨道在低索引轨道中的动画混合比例</s:String>
<s:String x:Key="Str_Debug">调试</s:String>
<s:String x:Key="Str_DebugTexture">Texture</s:String>
@@ -106,6 +116,7 @@
<s:String x:Key="Str_Zoom">缩放</s:String>
<s:String x:Key="Str_Rotation">旋转(角度)</s:String>
<s:String x:Key="Str_MaxFps">最大帧率</s:String>
<s:String x:Key="Str_MaxFpsTooltip">预览画面的最大帧率,设置为 0 时则无帧率限制</s:String>
<s:String x:Key="Str_PlaySpeed">播放速度</s:String>
<s:String x:Key="Str_RenderSelectedOnly">仅渲染选中</s:String>
<s:String x:Key="Str_ShowAxis">显示坐标轴</s:String>
@@ -149,6 +160,10 @@
<s:String x:Key="Str_InvalidMaxResolution">使用自动分辨率时需要提供有效的最大分辨率</s:String>
<s:String x:Key="Str_FFmpegFormatRequired">必须指定 FFmpeg 导出格式</s:String>
<s:String x:Key="Str_ExportBaseArgs">基本参数</s:String>
<s:String x:Key="Str_ExportVideoArgs">视频参数</s:String>
<s:String x:Key="Str_ExportOtherArgs">其他参数</s:String>
<s:String x:Key="Str_ResolutionTooltip">画面分辨率,相关参数请在画面参数面板进行调整</s:String>
<s:String x:Key="Str_ExportSingle">导出单个</s:String>
<s:String x:Key="Str_ExportSingleTooltip">勾选后将所选模型在同一个画面上进行导出,且必须提供输出文件夹</s:String>
@@ -170,19 +185,21 @@
<s:String x:Key="Str_Fps">帧率</s:String>
<s:String x:Key="Str_ExportSpeed">导出速度</s:String>
<s:String x:Key="Str_ExportSpeedTooltip">导出速度因子, 仅影响模型的动作速度, 不影响导出时长和帧率等参数</s:String>
<s:String x:Key="Str_ExportSpeedTooltip">导出速度因子仅影响模型的动作速度不影响导出时长和帧率等参数</s:String>
<s:String x:Key="Str_KeepLastFrame">保留最后一帧</s:String>
<s:String x:Key="Str_KeepLastFrameTooltip">当设置保留最后一帧时,动图会更为连贯,但是帧数可能比预期帧数多 1</s:String>
<s:String x:Key="Str_VideoFormat">视频格式</s:String>
<s:String x:Key="Str_LoopPlay">循环播放</s:String>
<s:String x:Key="Str_LoopPlayTooltip">动图是否循环播放,仅对 Gif/Webp 格式生效</s:String>
<s:String x:Key="Str_LoopPlayTooltip" xml:space="preserve">[Gif/Webp]&#x0A;动图是否循环播放</s:String>
<s:String x:Key="Str_QualityParameter">质量参数</s:String>
<s:String x:Key="Str_QualityParameterTooltip">质量参数,取值范围 0-100越高质量越好, 仅对 Webp 格式生效</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">无损压缩, 会忽略质量参数, 仅对 Webp 格式生效</s:String>
<s:String x:Key="Str_LosslessParamTooltip" xml:space="preserve">[Webp]&#x0A;无损压缩会忽略质量参数</s:String>
<s:String x:Key="Str_CrfParameter">CRF 参数</s:String>
<s:String x:Key="Str_CrfParameterTooltip">CRF 参数,取值范围 0-63越小质量越高,仅对 Mp4/Webm/Mkv 格式生效</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>
<s:String x:Key="Str_ProfileParameterTooltip" xml:space="preserve">[Mov]&#x0A;Profile 参数,取值集合为 -1 到 5 之间的整数,&#x0A;-1 表示自动0-5 取值越高质量越高,&#x0A;仅在取值大于等于 4 时可以编码透明度通道</s:String>
<s:String x:Key="Str_FFmpegFormat">导出格式</s:String>
<s:String x:Key="Str_FFmpegFormatTooltip">FFmpeg 导出格式,等价于参数 “-f”例如 “mp4”、“webm”</s:String>
@@ -215,7 +232,10 @@
<s:String x:Key="Str_SpineLoadPreference">模型加载选项</s:String>
<s:String x:Key="Str_RendererPreference">预览画面选项</s:String>
<s:String x:Key="Str_AppPreference">应用程序选项</s:String>
<s:String x:Key="Str_AssociateFileSuffix">关联文件后缀</s:String>
<s:String x:Key="Str_Language">语言</s:String>
</ResourceDictionary>

View File

@@ -61,6 +61,18 @@ namespace SpineViewer.Services
return dialog.ShowDialog() ?? false;
}
public static bool ShowOpenFileDialog(out string? fileName, string title = null, string filter = "")
{
var dialog = new OpenFileDialog() { Title = title, Filter = filter };
if (dialog.ShowDialog() is true)
{
fileName = dialog.FileName;
return true;
}
fileName = null;
return false;
}
/// <summary>
/// 获取用户选择的文件夹
/// </summary>

View File

@@ -7,19 +7,22 @@
<TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.15.10</Version>
<Version>0.15.17</Version>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);NETSDK1206</NoWarn>
<ApplicationIcon>appicon.ico</ApplicationIcon>
<ApplicationIcon>Resources\Images\spineviewer.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<Content Include="appicon.ico" />
<Content Include="Resources\Images\skel.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Images\spineviewer.ico" />
</ItemGroup>
<ItemGroup>

View File

@@ -35,6 +35,9 @@ namespace SpineViewer.ViewModels.Exporters
public int Crf { get => _crf; set => SetProperty(ref _crf, Math.Clamp(value, 0, 63)); }
protected int _crf = 23;
public int Profile { get => _profile; set => SetProperty(ref _profile, Math.Clamp(value, -1, 5)); }
protected int _profile = 5;
private string FormatSuffix => $".{_format.ToString().ToLower()}";
protected override void Export(SpineObjectModel[] models)
@@ -60,7 +63,8 @@ namespace SpineViewer.ViewModels.Exporters
Loop = _loop,
Quality = _quality,
Lossless = _lossless,
Crf = _crf
Crf = _crf,
Profile = _profile,
};
// 非自动分辨率则直接用预览画面的视区参数

View File

@@ -1,13 +1,16 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
using NLog;
using Spine.SpineWrappers;
using SpineViewer.Models;
using SpineViewer.Natives;
using SpineViewer.Services;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
@@ -23,6 +26,10 @@ namespace SpineViewer.ViewModels.MainWindow
/// </summary>
public static readonly string PreferenceFilePath = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), "preference.json");
private static readonly string SkelFileDescription = "SpineViewer File";
private static readonly string SkelIconFilePath = Path.Combine(App.ProcessDirectory, "Resources\\Images\\skel.ico");
private static readonly string ShellOpenCommand = $"\"{App.ProcessPath}\" \"%1\"";
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
private readonly MainWindowViewModel _vmMain;
@@ -63,8 +70,19 @@ namespace SpineViewer.ViewModels.MainWindow
/// </summary>
public void LoadPreference()
{
if (JsonHelper.Deserialize<PreferenceModel>(PreferenceFilePath, out var obj, true))
Preference = obj;
if (JsonHelper.Deserialize<PreferenceModel>(PreferenceFilePath, out var obj, true))
{
try
{
Preference = obj;
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to load some prefereneces, {0}", ex.Message);
}
}
}
/// <summary>
@@ -94,6 +112,7 @@ namespace SpineViewer.ViewModels.MainWindow
DebugClippings = DebugClippings,
RenderSelectedOnly = RenderSelectedOnly,
AssociateFileSuffix = AssociateFileSuffix,
AppLanguage = AppLanguage,
};
}
@@ -118,6 +137,7 @@ namespace SpineViewer.ViewModels.MainWindow
DebugClippings = value.DebugClippings;
RenderSelectedOnly = value.RenderSelectedOnly;
AssociateFileSuffix = value.AssociateFileSuffix;
AppLanguage = value.AppLanguage;
}
}
@@ -230,6 +250,71 @@ namespace SpineViewer.ViewModels.MainWindow
set => SetProperty(_vmMain.SFMLRendererViewModel.RenderSelectedOnly, value, v => _vmMain.SFMLRendererViewModel.RenderSelectedOnly = v);
}
public bool AssociateFileSuffix
{
get
{
try
{
// 检查 .skel 的 ProgID
using (var key = Registry.CurrentUser.OpenSubKey(@"Software\Classes\.skel"))
{
var progIdValue = key?.GetValue("") as string;
if (!string.Equals(progIdValue, App.ProgId, StringComparison.OrdinalIgnoreCase))
return false;
}
// 检查 command 指令是否相同
using (var key = Registry.CurrentUser.OpenSubKey($@"Software\Classes\{App.ProgId}\shell\open\command"))
{
var command = key?.GetValue("") as string;
if (string.IsNullOrWhiteSpace(command))
return false;
return command == ShellOpenCommand;
}
}
catch
{
return false;
}
}
set
{
SetProperty(AssociateFileSuffix, value, v =>
{
if (v)
{
// 文件关联
using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Classes\.skel"))
{
key?.SetValue("", App.ProgId);
}
using (var key = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{App.ProgId}"))
{
key?.SetValue("", SkelFileDescription);
using (var iconKey = key?.CreateSubKey("DefaultIcon"))
{
iconKey?.SetValue("", $"\"{SkelIconFilePath}\"");
}
using (var shellKey = key?.CreateSubKey(@"shell\open\command"))
{
shellKey?.SetValue("", ShellOpenCommand);
}
}
}
else
{
// 删除关联
Registry.CurrentUser.DeleteSubKeyTree(@"Software\Classes\.skel", false);
Registry.CurrentUser.DeleteSubKeyTree($@"Software\Classes\{App.ProgId}", false);
}
Shell32.SHChangeNotify(Shell32.SHCNE_ASSOCCHANGED, Shell32.SHCNF_IDLIST, IntPtr.Zero, IntPtr.Zero);
});
}
}
public AppLanguage AppLanguage
{
get => ((App)App.Current).Language;

View File

@@ -86,6 +86,14 @@ namespace SpineViewer.ViewModels.MainWindow
/// </summary>
public event NotifyCollectionChangedEventHandler? RequestSelectionChanging;
public void SetResolution(uint x, uint y)
{
var lastRes = _renderer.Resolution;
_renderer.Resolution = new(x, y);
if (lastRes.X != x) OnPropertyChanged(nameof(ResolutionX));
if (lastRes.Y != y) OnPropertyChanged(nameof(ResolutionY));
}
public uint ResolutionX
{
get => _renderer.Resolution.X;
@@ -455,8 +463,7 @@ namespace SpineViewer.ViewModels.MainWindow
}
set
{
ResolutionX = value.ResolutionX;
ResolutionY = value.ResolutionY;
SetResolution(value.ResolutionX, value.ResolutionY);
CenterX = value.CenterX;
CenterY = value.CenterY;
Zoom = value.Zoom;

View File

@@ -11,6 +11,7 @@ using SpineViewer.ViewModels.Exporters;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Text;
@@ -45,6 +46,11 @@ namespace SpineViewer.ViewModels.MainWindow
_customFFmpegExporterViewModel = new(_vmMain);
}
/// <summary>
/// 请求选中项发生变化
/// </summary>
public event NotifyCollectionChangedEventHandler? RequestSelectionChanging;
/// <summary>
/// 单帧导出 ViewModel
/// </summary>
@@ -101,7 +107,12 @@ namespace SpineViewer.ViewModels.MainWindow
private void AddSpineObject_Execute()
{
MessagePopupService.Info("Not Implemented, please drag files into here or add them from clipboard :)");
if (!DialogService.ShowOpenFileDialog(out var skelFileName, AppResource.Str_OpenSkelFileTitle))
return;
if (!DialogService.ShowOpenFileDialog(out var atlasFileName, AppResource.Str_OpenAtlasFileTitle))
return;
AddSpineObject(skelFileName, atlasFileName);
_logger.LogCurrentProcessMemoryUsage();
}
/// <summary>
@@ -138,6 +149,34 @@ namespace SpineViewer.ViewModels.MainWindow
return true;
}
/// <summary>
/// 移除全部模型
/// </summary>
public RelayCommand<IList?> Cmd_RemoveAllSpineObject => _cmd_RemoveAllSpineObject ??= new(RemoveAllSpineObject_Execute, RemoveAllSpineObject_CanExecute);
private RelayCommand<IList?>? _cmd_RemoveAllSpineObject;
private void RemoveAllSpineObject_Execute(IList? args)
{
if (!RemoveAllSpineObject_CanExecute(args)) return;
if (!MessagePopupService.Quest(string.Format(AppResource.Str_RemoveItemsQuest, args.Count)))
return;
lock (_spineObjectModels.Lock)
{
foreach (var sp in _spineObjectModels)
sp.Dispose();
_spineObjectModels.Clear();
}
}
private bool RemoveAllSpineObject_CanExecute(IList? args)
{
if (args is null) return false;
if (args.Count <= 0) return false;
return true;
}
/// <summary>
/// 从剪贴板文件列表添加模型
/// </summary>
@@ -457,7 +496,7 @@ namespace SpineViewer.ViewModels.MainWindow
{
if (ct.IsCancellationRequested) break;
var skelPath = paths[i];
var skelPath = paths[totalCount - 1 - i]; // 从后往前添加, 每次插入到列表的第一个
reporter.ProgressText = $"[{i}/{totalCount}] {skelPath}";
if (AddSpineObject(skelPath))
@@ -480,7 +519,7 @@ namespace SpineViewer.ViewModels.MainWindow
}
/// <summary>
/// 安全地在末尾添加一个模型, 发生错误会输出日志
/// 安全地在列表头添加一个模型, 发生错误会输出日志
/// </summary>
/// <returns>是否添加成功</returns>
private bool AddSpineObject(string skelPath, string? atlasPath = null)
@@ -488,7 +527,20 @@ namespace SpineViewer.ViewModels.MainWindow
try
{
var sp = new SpineObjectModel(skelPath, atlasPath);
lock (_spineObjectModels.Lock) _spineObjectModels.Add(sp);
lock (_spineObjectModels.Lock) _spineObjectModels.Insert(0, sp);
if (Application.Current.Dispatcher.CheckAccess())
{
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
}
else
{
Application.Current.Dispatcher.Invoke(() =>
{
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
});
}
return true;
}
catch (Exception ex)
@@ -499,22 +551,6 @@ namespace SpineViewer.ViewModels.MainWindow
return false;
}
private bool AddSpineObject(SpineObjectWorkspaceConfigModel cfg)
{
try
{
var sp = new SpineObjectModel(cfg);
lock (_spineObjectModels.Lock) _spineObjectModels.Add(sp);
return true;
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to load: {0}, {1}", cfg.SkelPath, ex.Message);
}
return false;
}
public List<SpineObjectWorkspaceConfigModel> LoadedSpineObjects
{
get
@@ -577,7 +613,7 @@ 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))
@@ -605,5 +641,38 @@ namespace SpineViewer.ViewModels.MainWindow
sp.ResetAnimationsTime();
}
}
/// <summary>
/// 安全地在列表头添加一个模型, 发生错误会输出日志
/// </summary>
/// <returns>是否添加成功</returns>
private bool AddSpineObject(SpineObjectWorkspaceConfigModel cfg)
{
try
{
var sp = new SpineObjectModel(cfg);
lock (_spineObjectModels.Lock) _spineObjectModels.Insert(0, sp);
if (Application.Current.Dispatcher.CheckAccess())
{
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
}
else
{
Application.Current.Dispatcher.Invoke(() =>
{
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
});
}
return true;
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to load: {0}, {1}", cfg.SkelPath, ex.Message);
}
return false;
}
}
}

View File

@@ -31,7 +31,7 @@ namespace SpineViewer.ViewModels.MainWindow
foreach (var obj in _selectedObjects)
{
obj.PropertyChanged -= SingleModel_PropertyChanged;
obj.AnimationChanged -= SingleModel_AnimationChanged;
obj.TrackPropertyChanged -= SingleModel_TrackPropChanged;
}
_skins.Clear();
_slots.Clear();
@@ -44,7 +44,7 @@ namespace SpineViewer.ViewModels.MainWindow
foreach (var obj in _selectedObjects)
{
obj.PropertyChanged += SingleModel_PropertyChanged;
obj.AnimationChanged += SingleModel_AnimationChanged;
obj.TrackPropertyChanged += SingleModel_TrackPropChanged;
}
IEnumerable<string> commonSkinNames = _selectedObjects[0].Skins;
@@ -74,6 +74,7 @@ namespace SpineViewer.ViewModels.MainWindow
OnPropertyChanged(nameof(IsShown));
OnPropertyChanged(nameof(UsePma));
OnPropertyChanged(nameof(Physics));
OnPropertyChanged(nameof(TimeScale));
OnPropertyChanged(nameof(Scale));
OnPropertyChanged(nameof(FlipX));
@@ -217,6 +218,25 @@ namespace SpineViewer.ViewModels.MainWindow
}
}
public float? TimeScale
{
get
{
if (_selectedObjects.Length <= 0) return null;
var val = _selectedObjects[0].TimeScale;
if (_selectedObjects.Skip(1).Any(it => it.TimeScale != val)) return null;
return val;
}
set
{
if (_selectedObjects.Length <= 0) return;
if (value is null) return;
foreach (var sp in _selectedObjects) sp.TimeScale = (float)value;
OnPropertyChanged();
}
}
public float? Scale
{
get
@@ -384,6 +404,27 @@ namespace SpineViewer.ViewModels.MainWindow
);
private RelayCommand<IList?>? _cmd_InsertTrack;
public RelayCommand<IList?>? Cmd_ClearTrack => _cmd_ClearTrack ??= new(
args =>
{
if (_selectedObjects.Length <= 0) return;
if (args is null) return;
if (args.Count <= 0) return;
foreach (var vm in args.OfType<AnimationTrackViewModel>())
foreach (var sp in _selectedObjects)
sp.ClearTrack(vm.TrackIndex);
},
args =>
{
if (_selectedObjects.Length <= 0) return false;
if (args is null) return false;
if (args.Count <= 0) return false;
return true;
}
);
private RelayCommand<IList?>? _cmd_ClearTrack;
public bool? DebugTexture
{
get
@@ -574,58 +615,67 @@ namespace SpineViewer.ViewModels.MainWindow
}
}
/// <summary>
/// 监听单个模型属性发生变化, 则更新聚合属性值
/// </summary>
private void SingleModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
private static readonly Dictionary<string, string> _singleModelPropertyMap = new()
{
if (e.PropertyName == nameof(SpineObjectModel.IsShown)) OnPropertyChanged(nameof(IsShown));
else if (e.PropertyName == nameof(SpineObjectModel.UsePma)) OnPropertyChanged(nameof(UsePma));
else if (e.PropertyName == nameof(SpineObjectModel.Physics)) OnPropertyChanged(nameof(Physics));
{ nameof(SpineObjectModel.IsShown), nameof(IsShown) },
{ nameof(SpineObjectModel.UsePma), nameof(UsePma) },
{ nameof(SpineObjectModel.Physics), nameof(Physics) },
{ nameof(SpineObjectModel.TimeScale), nameof(TimeScale) },
else if (e.PropertyName == nameof(SpineObjectModel.Scale)) OnPropertyChanged(nameof(Scale));
else if (e.PropertyName == nameof(SpineObjectModel.FlipX)) OnPropertyChanged(nameof(FlipX));
else if (e.PropertyName == nameof(SpineObjectModel.FlipY)) OnPropertyChanged(nameof(FlipY));
else if (e.PropertyName == nameof(SpineObjectModel.X)) OnPropertyChanged(nameof(X));
else if (e.PropertyName == nameof(SpineObjectModel.Y)) OnPropertyChanged(nameof(Y));
{ nameof(SpineObjectModel.Scale), nameof(Scale) },
{ nameof(SpineObjectModel.FlipX), nameof(FlipX) },
{ nameof(SpineObjectModel.FlipY), nameof(FlipY) },
{ nameof(SpineObjectModel.X), nameof(X) },
{ nameof(SpineObjectModel.Y), nameof(Y) },
// Skins 变化在 SkinViewModel 中监听
// Slots 变化在 SlotAttachmentViewModel 中监听
// AnimationTracks 变化在 AnimationTrackViewModel 中监听
else if (e.PropertyName == nameof(SpineObjectModel.DebugTexture)) OnPropertyChanged(nameof(DebugTexture));
else if (e.PropertyName == nameof(SpineObjectModel.DebugBounds)) OnPropertyChanged(nameof(DebugBounds));
else if (e.PropertyName == nameof(SpineObjectModel.DebugBones)) OnPropertyChanged(nameof(DebugBones));
else if (e.PropertyName == nameof(SpineObjectModel.DebugRegions)) OnPropertyChanged(nameof(DebugRegions));
else if (e.PropertyName == nameof(SpineObjectModel.DebugMeshHulls)) OnPropertyChanged(nameof(DebugMeshHulls));
else if (e.PropertyName == nameof(SpineObjectModel.DebugMeshes)) OnPropertyChanged(nameof(DebugMeshes));
else if (e.PropertyName == nameof(SpineObjectModel.DebugBoundingBoxes)) OnPropertyChanged(nameof(DebugBoundingBoxes));
else if (e.PropertyName == nameof(SpineObjectModel.DebugPaths)) OnPropertyChanged(nameof(DebugPaths));
else if (e.PropertyName == nameof(SpineObjectModel.DebugPoints)) OnPropertyChanged(nameof(DebugPoints));
else if (e.PropertyName == nameof(SpineObjectModel.DebugClippings)) OnPropertyChanged(nameof(DebugClippings));
{ nameof(SpineObjectModel.DebugTexture), nameof(DebugTexture) },
{ nameof(SpineObjectModel.DebugBounds), nameof(DebugBounds) },
{ nameof(SpineObjectModel.DebugBones), nameof(DebugBones) },
{ nameof(SpineObjectModel.DebugRegions), nameof(DebugRegions) },
{ nameof(SpineObjectModel.DebugMeshHulls), nameof(DebugMeshHulls) },
{ nameof(SpineObjectModel.DebugMeshes), nameof(DebugMeshes) },
{ nameof(SpineObjectModel.DebugBoundingBoxes), nameof(DebugBoundingBoxes) },
{ nameof(SpineObjectModel.DebugPaths), nameof(DebugPaths) },
{ nameof(SpineObjectModel.DebugPoints), nameof(DebugPoints) },
{ nameof(SpineObjectModel.DebugClippings), nameof(DebugClippings) },
};
/// <summary>
/// 监听单个模型属性发生变化, 则更新聚合属性值
/// </summary>
private void SingleModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (_singleModelPropertyMap.TryGetValue(e.PropertyName, out var targetProperty))
{
OnPropertyChanged(targetProperty);
}
}
/// <summary>
/// 监听单个模型动画轨道发生变化, 则重建聚合后的动画列表
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SingleModel_AnimationChanged(object? sender, AnimationChangedEventArgs e)
private void SingleModel_TrackPropChanged(object? sender, TrackPropertyChangedEventArgs e)
{
// XXX: 这里应该有更好的实现, 当 e.AnimationName == null 的时候代表删除轨道需要重新构建列表
// 但是目前无法识别是否增加了轨道, 因此总是重建列表
// 由于某些原因, 直接使用 Clear 会和 UI 逻辑冲突产生报错, 因此需要放到 Dispatcher 里延迟执行
Application.Current.Dispatcher.BeginInvoke(
() =>
{
_animationTracks.Clear();
IEnumerable<int> commonTrackIndices = _selectedObjects[0].GetTrackIndices();
foreach (var obj in _selectedObjects.Skip(1)) commonTrackIndices = commonTrackIndices.Intersect(obj.GetTrackIndices());
foreach (var idx in commonTrackIndices) _animationTracks.Add(new(idx, _selectedObjects));
}
);
if (e.PropertyName == nameof(TrackPropertyChangedEventArgs.AnimationName))
{
// XXX: 这里应该有更好的实现, 当 e.AnimationName == null 的时候代表删除轨道需要重新构建列表
// 但是目前无法识别是否增加了轨道, 因此总是重建列表
// 由于某些原因, 直接使用 Clear 会和 UI 逻辑冲突产生报错, 因此需要放到 Dispatcher 里延迟执行
Application.Current.Dispatcher.BeginInvoke(
() =>
{
_animationTracks.Clear();
IEnumerable<int> commonTrackIndices = _selectedObjects[0].GetTrackIndices();
foreach (var obj in _selectedObjects.Skip(1)) commonTrackIndices = commonTrackIndices.Intersect(obj.GetTrackIndices());
foreach (var idx in commonTrackIndices) _animationTracks.Add(new(idx, _selectedObjects));
}
);
}
}
public class SkinViewModel : ObservableObject
@@ -798,21 +848,36 @@ namespace SpineViewer.ViewModels.MainWindow
// 使用弱引用, 则此 ViewModel 被释放时无需显式退订事件
foreach (var sp in _spines)
{
WeakEventManager<SpineObjectModel, AnimationChangedEventArgs>.AddHandler(
WeakEventManager<SpineObjectModel, TrackPropertyChangedEventArgs>.AddHandler(
sp,
nameof(sp.AnimationChanged),
SingleModel_AnimationChanged
nameof(sp.TrackPropertyChanged),
SingleModel_TrackPropChanged
);
}
}
public RelayCommand Cmd_ClearTrack => _cmd_ClearTrack ??= new(() => { foreach (var sp in _spines) sp.ClearTrack(_trackIndex); });
private RelayCommand? _cmd_ClearTrack;
public ReadOnlyCollection<string> AnimationNames => _animationNames.AsReadOnly();
public int TrackIndex => _trackIndex;
public float? AnimationDuration
{
get
{
if (_spines.Length <= 0) return null;
var ani = _spines[0].GetAnimation(_trackIndex);
if (ani is null) return null;
var val = _spines[0].GetAnimationDuration(ani);
foreach (var sp in _spines.Skip(1))
{
var a = sp.GetAnimation(_trackIndex);
if (a is null) return null;
if (sp.GetAnimationDuration(a) != val) return null;
}
return val;
}
}
public string? AnimationName
{
get
@@ -834,27 +899,54 @@ namespace SpineViewer.ViewModels.MainWindow
}
}
public float? AnimationDuration
public float? TrackTimeScale
{
get
{
// XXX: 空轨道和多选不相同都会返回 null
if (_spines.Length <= 0) return null;
var ani = _spines[0].GetAnimation(_trackIndex);
if (ani is null) return null;
var val = _spines[0].GetAnimationDuration(ani);
foreach (var sp in _spines.Skip(1))
{
var a = sp.GetAnimation(_trackIndex);
if (a is null) return null;
if (sp.GetAnimationDuration(a) != val) return null;
}
var val = _spines[0].GetTrackTimeScale(_trackIndex);
if (_spines.Skip(1).Any(it => it.GetTrackTimeScale(_trackIndex) != val)) return null;
return val;
}
set
{
if (_spines.Length <= 0) return;
if (value is null) return;
foreach (var sp in _spines) sp.SetTrackTimeScale(_trackIndex, (float)value);
OnPropertyChanged();
}
}
private void SingleModel_AnimationChanged(object? sender, AnimationChangedEventArgs e)
public float? TrackAlpha
{
if (e.TrackIndex == _trackIndex) OnPropertyChanged(nameof(AnimationName));
get
{
// XXX: 空轨道和多选不相同都会返回 null
if (_spines.Length <= 0) return null;
var val = _spines[0].GetTrackAlpha(_trackIndex);
if (_spines.Skip(1).Any(it => it.GetTrackAlpha(_trackIndex) != val)) return null;
return val;
}
set
{
if (_spines.Length <= 0) return;
if (value is null) return;
foreach (var sp in _spines) sp.SetTrackAlpha(_trackIndex, (float)value);
OnPropertyChanged();
}
}
private void SingleModel_TrackPropChanged(object? sender, TrackPropertyChangedEventArgs e)
{
if (e.TrackIndex == _trackIndex)
{
if (e.PropertyName == nameof(TrackPropertyChangedEventArgs.AnimationName)) OnPropertyChanged(nameof(AnimationName));
else if (e.PropertyName == nameof(TrackPropertyChangedEventArgs.TimeScale)) OnPropertyChanged(nameof(TrackTimeScale));
else if (e.PropertyName == nameof(TrackPropertyChangedEventArgs.Alpha)) OnPropertyChanged(nameof(TrackAlpha));
}
}
}
}

View File

@@ -3,13 +3,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
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:CustomFFmpegExporterViewModel}"
mc:Ignorable="d"
Title="{DynamicResource Str_CustomFFmpegExporterTitle}"
Width="450"
Height="650"
Height="800"
ShowInTaskbar="False"
WindowStartupLocation="CenterOwner">
<DockPanel>
@@ -27,145 +28,186 @@
</Border>
<Border>
<Border.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>
<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"/>
<Setter Property="Margin" Value="0 5 0 10"/>
</Style>
</Border.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<Grid Margin="30 10">
<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"/>
<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"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="60"/>
</Grid.RowDefinitions>
<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>
<!-- 水平分辨率 -->
<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}"/>
<!-- 水平分辨率 -->
<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}"/>
<!-- 垂直分辨率 -->
<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}"/>
<!-- 垂直分辨率 -->
<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}"/>
<!-- 是否导出单个 -->
<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}"/>
<!-- 是否导出单个 -->
<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}"/>
<!-- 输出文件夹 -->
<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>
<!-- 输出文件夹 -->
<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>
<!-- 背景颜色 -->
<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>
<!-- 背景颜色 -->
<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>
<!-- 四周边距 -->
<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}"/>
<!-- 四周边距 -->
<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}"/>
<!-- 自动分辨率 -->
<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}"/>
<!-- 自动分辨率 -->
<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}"/>
<!-- 最大分辨率 -->
<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}"/>
<!-- 最大分辨率 -->
<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>
</GroupBox>
<Separator Grid.Row="8" Grid.ColumnSpan="2" Height="10"/>
<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>
<!-- 导出时长 -->
<Label Grid.Row="9" Grid.Column="0" Content="{DynamicResource Str_Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
<TextBox Grid.Row="9" Grid.Column="1" Text="{Binding Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
<!-- 导出时长 -->
<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}"/>
<!-- 导出帧率 -->
<Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_Fps}"/>
<TextBox Grid.Row="10" Grid.Column="1" Text="{Binding Fps}"/>
<!-- 导出帧率 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_Fps}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Fps}"/>
<!-- 导出速度 -->
<Label Grid.Row="11" Grid.Column="0" Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
<TextBox Grid.Row="11" Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
<!-- 导出速度 -->
<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}"/>
<!-- 是否保留最后一帧 -->
<Label Grid.Row="12" Grid.Column="0" Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
<ToggleButton Grid.Row="12" Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
<!-- 是否保留最后一帧 -->
<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>
</GroupBox>
<Separator Grid.Row="13" Grid.ColumnSpan="2" Height="10"/>
<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>
<!-- 导出格式 -->
<Label Grid.Row="14" Grid.Column="0" Content="{DynamicResource Str_FFmpegFormat}" ToolTip="{DynamicResource Str_FFmpegFormatTooltip}"/>
<TextBox Grid.Row="14" Grid.Column="1" Text="{Binding Format}" ToolTip="{DynamicResource Str_FFmpegFormatTooltip}"/>
<!-- 导出格式 -->
<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}"/>
<!-- 编码器 -->
<Label Grid.Row="15" Grid.Column="0" Content="{DynamicResource Str_FFmpegCodec}" ToolTip="{DynamicResource Str_FFmpegCodecTooltip}"/>
<TextBox Grid.Row="15" Grid.Column="1" Text="{Binding Codec}" ToolTip="{DynamicResource Str_FFmpegCodecTooltip}"/>
<!-- 编码器 -->
<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}"/>
<!-- 像素格式 -->
<Label Grid.Row="16" Grid.Column="0" Content="{DynamicResource Str_FFmpegPixelFormat}" ToolTip="{DynamicResource Str_FFmpegPixelFormatTooltip}"/>
<TextBox Grid.Row="16" Grid.Column="1" Text="{Binding PixelFormat}" ToolTip="{DynamicResource Str_FFmpegPixelFormatTooltip}"/>
<!-- 像素格式 -->
<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}"/>
<!-- 比特率 -->
<Label Grid.Row="17" Grid.Column="0" Content="{DynamicResource Str_FFmpegBitrate}" ToolTip="{DynamicResource Str_FFmpegBitrateTooltip}"/>
<TextBox Grid.Row="17" Grid.Column="1" Text="{Binding Bitrate}" ToolTip="{DynamicResource Str_FFmpegBitrateTooltip}"/>
<!-- 比特率 -->
<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}"/>
<!-- 滤镜 -->
<Label Grid.Row="18" Grid.Column="0" Content="{DynamicResource Str_FFmpegFilter}" ToolTip="{DynamicResource Str_FFmpegFilterTooltip}"/>
<TextBox Grid.Row="18" Grid.Column="1" Text="{Binding Filter}" ToolTip="{DynamicResource Str_FFmpegFilterTooltip}"/>
<!-- 滤镜 -->
<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}"/>
<!-- 自定义参数 -->
<Label Grid.Row="19" Grid.Column="0"
VerticalAlignment="Top"
Content="{DynamicResource Str_FFmpegCustomArgs}"
ToolTip="{DynamicResource Str_FFmpegCustomArgsTooltip}"/>
<TextBox Grid.Row="19" Grid.Column="1"
HorizontalContentAlignment="Left"
VerticalContentAlignment="Top"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto"
Text="{Binding CustomArgs}"
ToolTip="{DynamicResource Str_FFmpegCustomArgsTooltip}"/>
</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>
</GroupBox>
</StackPanel>
</ScrollViewer>
</Border>
</DockPanel>

View File

@@ -3,13 +3,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
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}"
mc:Ignorable="d"
Title="{DynamicResource Str_FFmpegVideoExporterTitle}"
Width="450"
Height="580"
Height="750"
ShowInTaskbar="False"
WindowStartupLocation="CenterOwner">
<DockPanel>
@@ -27,133 +28,165 @@
</Border>
<Border>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<Grid Margin="30 10">
<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"/>
<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"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<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>
<Border.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>
<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"/>
<Setter Property="Margin" Value="0 5 0 10"/>
</Style>
</Border.Resources>
<!-- 水平分辨率 -->
<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}"/>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<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>
<!-- 垂直分辨率 -->
<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}"/>
<!-- 水平分辨率 -->
<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}"/>
<!-- 是否导出单个 -->
<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}"/>
<!-- 垂直分辨率 -->
<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}"/>
<!-- 输出文件夹 -->
<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>
<!-- 是否导出单个 -->
<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}"/>
<!-- 背景颜色 -->
<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>
<!-- 输出文件夹 -->
<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>
<!-- 四周边距 -->
<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}"/>
<!-- 背景颜色 -->
<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>
<!-- 自动分辨率 -->
<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}"/>
<!-- 四周边距 -->
<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}"/>
<!-- 最大分辨率 -->
<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}"/>
<!-- 自动分辨率 -->
<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}"/>
<Separator Grid.Row="8" Grid.ColumnSpan="2" Height="10"/>
<!-- 最大分辨率 -->
<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>
</GroupBox>
<!-- 导出时长 -->
<Label Grid.Row="9" Grid.Column="0" Content="{DynamicResource Str_Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
<TextBox Grid.Row="9" Grid.Column="1" Text="{Binding Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
<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>
<!-- 导出帧率 -->
<Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_Fps}"/>
<TextBox Grid.Row="10" Grid.Column="1" Text="{Binding Fps}"/>
<!-- 导出时长 -->
<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}"/>
<!-- 导出速度 -->
<Label Grid.Row="11" Grid.Column="0" Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
<TextBox Grid.Row="11" Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
<!-- 导出帧率 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_Fps}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Fps}"/>
<!-- 是否保留最后一帧 -->
<Label Grid.Row="12" Grid.Column="0" Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
<ToggleButton Grid.Row="12" Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
<!-- 导出速度 -->
<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}"/>
<Separator Grid.Row="13" Grid.ColumnSpan="2" Height="10"/>
<!-- 是否保留最后一帧 -->
<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>
</GroupBox>
<!-- 视频格式 -->
<Label Grid.Row="14" Grid.Column="0" Content="{DynamicResource Str_VideoFormat}"/>
<ComboBox Grid.Row="14" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{Binding VideoFormatOptions}"/>
<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>
<!-- 动图是否循环 -->
<Label Grid.Row="15" Grid.Column="0" Content="{DynamicResource Str_LoopPlay}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/>
<ToggleButton Grid.Row="15" Grid.Column="1" IsChecked="{Binding Loop}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/>
<!-- 视频格式 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_VideoFormat}"/>
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{Binding VideoFormatOptions}"/>
<!-- 质量参数 -->
<Label Grid.Row="16" Grid.Column="0" Content="{DynamicResource Str_QualityParameter}" ToolTip="{DynamicResource Str_QualityParameterTooltip}"/>
<TextBox Grid.Row="16" Grid.Column="1" Text="{Binding Quality}" ToolTip="{DynamicResource Str_QualityParameterTooltip}"/>
<!-- 动图是否循环 -->
<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}"/>
<!-- 无损压缩 -->
<Label Grid.Row="17" Grid.Column="0" Content="{DynamicResource Str_LosslessParam}" ToolTip="{DynamicResource Str_LosslessParamTooltip}"/>
<ToggleButton Grid.Row="17" Grid.Column="1" IsChecked="{Binding Lossless}" ToolTip="{DynamicResource Str_LosslessParamTooltip}"/>
<!-- 质量参数 -->
<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}"/>
<!-- CRF 参数 -->
<Label Grid.Row="18" Grid.Column="0" Content="{DynamicResource Str_CrfParameter}" ToolTip="{DynamicResource Str_CrfParameterTooltip}"/>
<TextBox Grid.Row="18" Grid.Column="1" Text="{Binding Crf}" ToolTip="{DynamicResource Str_CrfParameterTooltip}"/>
</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}"/>
<!-- 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}"/>
<!-- 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>
</GroupBox>
</StackPanel>
</ScrollViewer>
</Border>
</DockPanel>
</Window>

View File

@@ -10,7 +10,7 @@
mc:Ignorable="d"
Title="{DynamicResource Str_FrameExporterTitle}"
Width="450"
Height="400"
Height="480"
ShowInTaskbar="False"
WindowStartupLocation="CenterOwner">
<DockPanel>
@@ -28,94 +28,112 @@
</Border>
<Border>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<Border.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>
<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"/>
<Setter Property="Margin" Value="0 5 0 10"/>
</Style>
</Border.Resources>
<Grid Margin="30 10">
<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"/>
<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"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<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>
<!-- 水平分辨率 -->
<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}"/>
<!-- 水平分辨率 -->
<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}"/>
<!-- 垂直分辨率 -->
<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}"/>
<!-- 垂直分辨率 -->
<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}"/>
<!-- 是否导出单个 -->
<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}"/>
<!-- 是否导出单个 -->
<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}"/>
<!-- 输出文件夹 -->
<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>
<!-- 输出文件夹 -->
<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>
<!-- 背景颜色 -->
<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>
<!-- 背景颜色 -->
<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>
<!-- 四周边距 -->
<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}"/>
<!-- 四周边距 -->
<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}"/>
<!-- 自动分辨率 -->
<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}"/>
<!-- 自动分辨率 -->
<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}"/>
<!-- 最大分辨率 -->
<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}"/>
<!-- 最大分辨率 -->
<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>
</GroupBox>
<Separator Grid.Row="8" Grid.ColumnSpan="2" Height="10"/>
<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>
<!-- 图像格式 -->
<Label Grid.Row="9" Grid.Column="0" Content="{DynamicResource Str_ImageFormat}"/>
<ComboBox Grid.Row="9" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{Binding FrameFormatOptions}"/>
<!-- 图像格式 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_ImageFormat}"/>
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{Binding FrameFormatOptions}"/>
<!-- 图像质量 -->
<Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_ImageQuality}" ToolTip="{DynamicResource Str_ImageQualityTooltip}"/>
<TextBox Grid.Row="10" Grid.Column="1" Text="{Binding Quality}" ToolTip="{DynamicResource Str_ImageQualityTooltip}"/>
</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>
</GroupBox>
</StackPanel>
</ScrollViewer>
</Border>
</DockPanel>

View File

@@ -3,13 +3,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:local="clr-namespace:SpineViewer.Views"
xmlns:vmexp="clr-namespace:SpineViewer.ViewModels.Exporters"
d:DataContext="{d:DesignInstance Type=vmexp:FrameSequenceExporterViewModel}"
mc:Ignorable="d"
Title="{DynamicResource Str_FrameSequenceExporterTitle}"
Width="450"
Height="450"
Height="550"
ShowInTaskbar="False"
WindowStartupLocation="CenterOwner">
<DockPanel>
@@ -27,104 +28,122 @@
</Border>
<Border>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<Border.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>
<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"/>
<Setter Property="Margin" Value="0 5 0 10"/>
</Style>
</Border.Resources>
<Grid Margin="30 10">
<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"/>
<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"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<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>
<!-- 水平分辨率 -->
<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}"/>
<!-- 水平分辨率 -->
<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}"/>
<!-- 垂直分辨率 -->
<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}"/>
<!-- 垂直分辨率 -->
<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}"/>
<!-- 是否导出单个 -->
<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}"/>
<!-- 是否导出单个 -->
<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}"/>
<!-- 输出文件夹 -->
<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>
<!-- 输出文件夹 -->
<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>
<!-- 背景颜色 -->
<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>
<!-- 背景颜色 -->
<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>
<!-- 四周边距 -->
<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}"/>
<!-- 四周边距 -->
<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}"/>
<!-- 自动分辨率 -->
<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}"/>
<!-- 自动分辨率 -->
<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}"/>
<!-- 最大分辨率 -->
<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}"/>
<!-- 最大分辨率 -->
<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>
</GroupBox>
<Separator Grid.Row="8" Grid.ColumnSpan="2" Height="10"/>
<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>
<!-- 导出时长 -->
<Label Grid.Row="9" Grid.Column="0" Content="{DynamicResource Str_Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
<TextBox Grid.Row="9" Grid.Column="1" Text="{Binding Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
<!-- 导出时长 -->
<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}"/>
<!-- 导出帧率 -->
<Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_Fps}"/>
<TextBox Grid.Row="10" Grid.Column="1" Text="{Binding Fps}"/>
<!-- 导出帧率 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_Fps}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Fps}"/>
<!-- 导出速度 -->
<Label Grid.Row="11" Grid.Column="0" Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
<TextBox Grid.Row="11" Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
<!-- 导出速度 -->
<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}"/>
<!-- 是否保留最后一帧 -->
<Label Grid.Row="12" Grid.Column="0" Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
<ToggleButton Grid.Row="12" Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
</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>
</GroupBox>
</StackPanel>
</ScrollViewer>
</Border>
</DockPanel>

View File

@@ -76,7 +76,7 @@
</Border>
<Border Grid.Row="1">
<Grid>
<Grid x:Name="_rootGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto" />
@@ -89,120 +89,9 @@
<!-- 功能页 -->
<TabControl x:Name="_mainTabControl" TabStripPlacement="Left">
<!-- 浏览页 -->
<TabItem Header="{DynamicResource Str_Explorer}" DataContext="{Binding ExplorerListViewModel}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<DockPanel>
<Grid DockPanel.Dock="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<hc:TextBox hc:InfoElement.Placeholder="{StaticResource Str_Filter}"
Text="{Binding FilterString, UpdateSourceTrigger=PropertyChanged}"/>
<Button Grid.Column="1"
hc:IconElement.Geometry="{StaticResource Geo_Folder}"
Command="{Binding Cmd_ChangeCurrentDirectory}"
ToolTip="{DynamicResource Str_ChangeCurrentDirectoryTooltip}"/>
<Button Grid.Column="2"
hc:IconElement.Geometry="{StaticResource Geo_ArrowRotateRight}"
Command="{Binding Cmd_RefreshItems}"
ToolTip="{DynamicResource Str_RefreshItemsTooltip}"/>
</Grid>
<StatusBar DockPanel.Dock="Bottom">
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource StrFmtCvter}" ConverterParameter="Str_ListViewStatusBar">
<Binding Path="Items.Count" ElementName="_spineFilesListBox"/>
<Binding Path="SelectedItems.Count" ElementName="_spineFilesListBox"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StatusBar>
<ListBox x:Name="_spineFilesListBox"
VirtualizingPanel.IsVirtualizing="True"
ItemsSource="{Binding ShownItems}"
DisplayMemberPath="FileName"
MouseLeftButtonDown="SpineFilesListBox_MouseLeftButtonDown">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding Cmd_SelectionChanged}"
CommandParameter="{Binding SelectedItems, ElementName=_spineFilesListBox}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header="{DynamicResource Str_AddSelectedItems}"
Command="{Binding Cmd_AddSelectedItems}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<Separator/>
<MenuItem Header="{DynamicResource Str_GeneratePreviewForSelected}"
Command="{Binding Cmd_GeneratePreviews}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<MenuItem Header="{StaticResource Str_DeletePreviewsForSelected}"
Command="{Binding Cmd_DeletePreviews}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
</DockPanel>
<GridSplitter Grid.Row="1" ResizeDirection="Rows"/>
<Grid Grid.Row="2" DataContext="{Binding SelectedItem}">
<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>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 文件目录 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_FileDirectory}"/>
<TextBox Grid.Row="0" Grid.Column="1"
Text="{Binding FileDirectory, Mode=OneWay}"
IsReadOnly="True"
ToolTip="{Binding Text, RelativeSource={RelativeSource Mode=Self}}"/>
<!-- 文件名 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_FileName}"/>
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding FileName, Mode=OneWay}"
IsReadOnly="True"/>
<!-- 预览图 -->
<Border Grid.Row="2" Grid.ColumnSpan="2" Background="#a0a0a0">
<Image Source="{Binding PreviewImage, Mode=OneWay}" Stretch="Uniform"/>
</Border>
</Grid>
</Grid>
</TabItem>
<!-- 模型列表页 -->
<TabItem Header="{DynamicResource Str_SpineObject}">
<Grid>
<Grid x:Name="_modelListGrid">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
@@ -258,13 +147,16 @@
<ContextMenu>
<MenuItem Header="{DynamicResource Str_AddSpineObject}"
Command="{Binding Cmd_AddSpineObject}"/>
<MenuItem Header="{DynamicResource Str_AddSpineObjectFromClipboard}"
InputGestureText="Ctrl+V"
Command="{Binding Cmd_AddSpineObjectFromClipboard}"/>
<MenuItem Header="{DynamicResource Str_RemoveSpineObject}"
InputGestureText="Delete"
Command="{Binding Cmd_RemoveSpineObject}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<MenuItem Header="{DynamicResource Str_AddSpineObjectFromClipboard}"
InputGestureText="Ctrl+V"
Command="{Binding Cmd_AddSpineObjectFromClipboard}"/>
<MenuItem Header="{DynamicResource Str_RemoveAllSpineObject}"
Command="{Binding Cmd_RemoveAllSpineObject}"
CommandParameter="{Binding PlacementTarget.Items, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<MenuItem Header="{DynamicResource Str_Reload}"
InputGestureText="Ctrl+R"
Command="{Binding Cmd_ReloadSpineObject}"
@@ -421,6 +313,7 @@
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 显示 -->
@@ -434,6 +327,10 @@
<!-- 物理 -->
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_Physics}"/>
<ComboBox Grid.Row="2" Grid.Column="1" SelectedValue="{Binding Physics}" ItemsSource="{Binding PhysicsOptions}"/>
<!-- 时间因子 -->
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_TimeScale}" ToolTip="{DynamicResource Str_TimeScaleTootltip}"/>
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding TimeScale}" ToolTip="{DynamicResource Str_TimeScaleTootltip}"/>
</Grid>
</TabItem>
@@ -503,7 +400,7 @@
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="{Binding Name}"/>
<Label Grid.Column="0" Content="{Binding Name}" Background="#bfffffff"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding Status}"/>
</Grid>
</DataTemplate>
@@ -541,7 +438,7 @@
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="{Binding SlotName}" HorizontalAlignment="Left"/>
<Label Grid.Column="0" Content="{Binding SlotName}" HorizontalAlignment="Left" Background="#bfffffff"/>
<ComboBox Grid.Column="1" SelectedValue="{Binding AttachmentName}" ItemsSource="{Binding AttachmentNames}"/>
<ToggleButton Grid.Column="2" IsChecked="{Binding Visible}"/>
</Grid>
@@ -568,6 +465,9 @@
<MenuItem Header="{DynamicResource Str_InsertTrack}"
Command="{Binding Cmd_InsertTrack}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<MenuItem Header="{DynamicResource Str_ClearTrack}"
Command="{Binding Cmd_ClearTrack}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
</ContextMenu>
</ListBox.ContextMenu>
@@ -575,19 +475,38 @@
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col0"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="ColTrackIdx"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="ColAniTime"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col2"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="{Binding TrackIndex}" HorizontalContentAlignment="Left"/>
<ComboBox Grid.Column="1" SelectedValue="{Binding AnimationName}" ItemsSource="{Binding AnimationNames}"/>
<Label Grid.Column="2"
Content="{Binding AnimationDuration}"
ContentStringFormat="{}{0:F3} s"/>
<Button Grid.Column="3"
Command="{Binding Cmd_ClearTrack}"
hc:IconElement.Geometry="{StaticResource Geo_TrashXmark}"/>
<Label Grid.Column="0" Content="{Binding TrackIndex}" HorizontalContentAlignment="Left" VerticalAlignment="Top" Background="#bfffffff"/>
<Label Grid.Column="1" Content="{Binding AnimationDuration}" VerticalAlignment="Top" ContentStringFormat="{}{0:F3} s"/>
<Expander Grid.Column="2" HorizontalContentAlignment="Stretch">
<Expander.Header>
<!-- hc 的模板自带左侧 10 的 padding, 此处用 -10 的 margin 来抵消去除 -->
<ComboBox Margin="-10 0 0 0" Grid.Column="2" SelectedValue="{Binding AnimationName}" ItemsSource="{Binding AnimationNames}"/>
</Expander.Header>
<Grid Margin="1 0 0 0">
<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_TrackTimeScale}" ToolTip="{DynamicResource Str_TrackTimeScaleTooltip}"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding TrackTimeScale, StringFormat='{}{0:F3}'}" ToolTip="{DynamicResource Str_TrackTimeScaleTooltip}"/>
<!-- Alpha 混合 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_TrackAlpha}" ToolTip="{DynamicResource Str_TrackAlphaTooltip}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding TrackAlpha, StringFormat='{}{0:F3}'}" ToolTip="{DynamicResource Str_TrackAlphaTooltip}"/>
</Grid>
</Expander>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
@@ -660,6 +579,117 @@
</Grid>
</TabItem>
<!-- 浏览页 -->
<TabItem Header="{DynamicResource Str_Explorer}" DataContext="{Binding ExplorerListViewModel}">
<Grid x:Name="_explorerGrid">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<DockPanel>
<Grid DockPanel.Dock="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<hc:TextBox hc:InfoElement.Placeholder="{StaticResource Str_Filter}"
Text="{Binding FilterString, UpdateSourceTrigger=PropertyChanged}"/>
<Button Grid.Column="1"
hc:IconElement.Geometry="{StaticResource Geo_Folder}"
Command="{Binding Cmd_ChangeCurrentDirectory}"
ToolTip="{DynamicResource Str_ChangeCurrentDirectoryTooltip}"/>
<Button Grid.Column="2"
hc:IconElement.Geometry="{StaticResource Geo_ArrowRotateRight}"
Command="{Binding Cmd_RefreshItems}"
ToolTip="{DynamicResource Str_RefreshItemsTooltip}"/>
</Grid>
<StatusBar DockPanel.Dock="Bottom">
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource StrFmtCvter}" ConverterParameter="Str_ListViewStatusBar">
<Binding Path="Items.Count" ElementName="_spineFilesListBox"/>
<Binding Path="SelectedItems.Count" ElementName="_spineFilesListBox"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StatusBar>
<ListBox x:Name="_spineFilesListBox"
VirtualizingPanel.IsVirtualizing="True"
ItemsSource="{Binding ShownItems}"
DisplayMemberPath="FileName"
MouseLeftButtonDown="SpineFilesListBox_MouseLeftButtonDown">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding Cmd_SelectionChanged}"
CommandParameter="{Binding SelectedItems, ElementName=_spineFilesListBox}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header="{DynamicResource Str_AddSelectedItems}"
Command="{Binding Cmd_AddSelectedItems}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<Separator/>
<MenuItem Header="{DynamicResource Str_GeneratePreviewForSelected}"
Command="{Binding Cmd_GeneratePreviews}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<MenuItem Header="{StaticResource Str_DeletePreviewsForSelected}"
Command="{Binding Cmd_DeletePreviews}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
</DockPanel>
<GridSplitter Grid.Row="1" ResizeDirection="Rows"/>
<Grid Grid.Row="2" DataContext="{Binding SelectedItem}">
<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>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 文件目录 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_FileDirectory}"/>
<TextBox Grid.Row="0" Grid.Column="1"
Text="{Binding FileDirectory, Mode=OneWay}"
IsReadOnly="True"
ToolTip="{Binding Text, RelativeSource={RelativeSource Mode=Self}}"/>
<!-- 文件名 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_FileName}"/>
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding FileName, Mode=OneWay}"
IsReadOnly="True"/>
<!-- 预览图 -->
<Border Grid.Row="2" Grid.ColumnSpan="2" Background="#a0a0a0">
<Image Source="{Binding PreviewImage, Mode=OneWay}" Stretch="Uniform"/>
</Border>
</Grid>
</Grid>
</TabItem>
<!-- 画面参数页 -->
<TabItem Header="{DynamicResource Str_Canvas}" DataContext="{Binding SFMLRendererViewModel}">
<TabItem.Resources>
@@ -695,6 +725,7 @@
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 水平分辨率 -->
@@ -729,22 +760,24 @@
<Label Grid.Row="7" Grid.Column="0" Content="{DynamicResource Str_FlipY}"/>
<ToggleButton Grid.Row="7" Grid.Column="1" IsChecked="{Binding FlipY}"/>
<Separator Grid.Row="8" Grid.Column="0" Grid.ColumnSpan="2" Margin="0 5"/>
<!-- 最大帧率 -->
<Label Grid.Row="8" Grid.Column="0" Content="{DynamicResource Str_MaxFps}"/>
<TextBox Grid.Row="8" Grid.Column="1" Text="{Binding MaxFps}"/>
<Label Grid.Row="9" Grid.Column="0" Content="{DynamicResource Str_MaxFps}" ToolTip="{DynamicResource Str_MaxFpsTooltip}"/>
<TextBox Grid.Row="9" Grid.Column="1" Text="{Binding MaxFps}" ToolTip="{DynamicResource Str_MaxFpsTooltip}"/>
<!-- 播放速度 -->
<Label Grid.Row="9" Grid.Column="0" Content="{DynamicResource Str_PlaySpeed}"/>
<TextBox Grid.Row="9" Grid.Column="1" Text="{Binding Speed}"/>
<Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_PlaySpeed}"/>
<TextBox Grid.Row="10" Grid.Column="1" Text="{Binding Speed}"/>
<!-- 显示坐标轴 -->
<Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_ShowAxis}"/>
<ToggleButton Grid.Row="10" Grid.Column="1" IsChecked="{Binding ShowAxis}"/>
<Label Grid.Row="11" Grid.Column="0" Content="{DynamicResource Str_ShowAxis}"/>
<ToggleButton Grid.Row="11" Grid.Column="1" IsChecked="{Binding ShowAxis}"/>
<!-- 背景颜色 -->
<Label Grid.Row="11" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
<TextBox Grid.Row="11" Grid.Column="1" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
<Label Grid.Row="12" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
<TextBox Grid.Row="12" Grid.Column="1" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
<!-- 背景图案 -->
<!-- 背景图案模式 -->
</Grid>
@@ -755,14 +788,14 @@
<GridSplitter Grid.Column="1" ResizeDirection="Columns"/>
<Border Grid.Column="2">
<Grid>
<Grid x:Name="_rightPanelGrid">
<Grid.RowDefinitions>
<RowDefinition Height="5*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid >
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>

View File

@@ -3,12 +3,15 @@ using NLog;
using NLog.Layouts;
using NLog.Targets;
using Spine;
using SpineViewer.Models;
using SpineViewer.Natives;
using SpineViewer.Resources;
using SpineViewer.Utils;
using SpineViewer.ViewModels.MainWindow;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Controls;
@@ -26,6 +29,11 @@ namespace SpineViewer.Views;
/// </summary>
public partial class MainWindow : Window
{
/// <summary>
/// 上一次状态文件保存路径
/// </summary>
public static readonly string LastStateFilePath = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), "laststate.json");
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
private ListViewItem? _listViewDragSourceItem = null;
private Point _listViewDragSourcePoint;
@@ -38,9 +46,10 @@ public partial class MainWindow : Window
InitializeLogConfiguration();
_vm = new (_renderPanel);
DataContext = _vm;
_vm.SpineObjectListViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging;
_vm.SFMLRendererViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging;
Loaded += MainWindow_Loaded;
ContentRendered += MainWindow_ContentRendered;
Closed += MainWindow_Closed;
}
@@ -48,13 +57,12 @@ public partial class MainWindow : Window
{
var vm = _vm.SFMLRendererViewModel;
_renderPanel.CanvasMouseWheelScrolled += vm.CanvasMouseWheelScrolled;
_renderPanel.CanvasMouseButtonPressed += vm.CanvasMouseButtonPressed;
_renderPanel.CanvasMouseButtonPressed += (s, e) => { vm.CanvasMouseButtonPressed(s, e); _spinesListView.Focus(); }; // 用户点击画布后强制转移焦点至列表
_renderPanel.CanvasMouseMove += vm.CanvasMouseMove;
_renderPanel.CanvasMouseButtonReleased += vm.CanvasMouseButtonReleased;
// 设置默认参数并启动渲染
vm.ResolutionX = 1500;
vm.ResolutionY = 1000;
vm.SetResolution(1500, 1000);
vm.Zoom = 0.75f;
vm.CenterX = 0;
vm.CenterY = 0;
@@ -64,14 +72,36 @@ public partial class MainWindow : Window
// 加载首选项
_vm.PreferenceViewModel.LoadPreference();
LoadLastState();
}
private void MainWindow_ContentRendered(object? sender, EventArgs e)
{
string[] args = Environment.GetCommandLineArgs();
if (args.Length > 1)
{
string[] filePaths = args.Skip(1).ToArray();
_vm.SpineObjectListViewModel.AddSpineObjectFromFileList(filePaths);
}
}
private void MainWindow_Closed(object? sender, EventArgs e)
{
SaveLastState();
var vm = _vm.SFMLRendererViewModel;
vm.StopRender();
}
/// <summary>
/// 给管道通信提供的打开文件外部调用方法
/// </summary>
public void OpenFiles(IEnumerable<string> filePaths)
{
_vm.SpineObjectListViewModel.AddSpineObjectFromFileList(filePaths);
}
/// <summary>
/// 初始化窗口日志器
/// </summary>
@@ -100,6 +130,63 @@ public partial class MainWindow : Window
LogManager.ReconfigExistingLoggers();
}
private void LoadLastState()
{
if (JsonHelper.Deserialize<LastStateModel>(LastStateFilePath, out var m, true))
{
Left = m.WindowLeft;
Top = m.WindowTop;
Width = m.WindowWidth;
Height = m.WindowHeight;
if (m.WindowState == WindowState.Maximized)
{
WindowState = WindowState.Maximized;
}
else
{
WindowState = WindowState.Normal;
}
_rootGrid.ColumnDefinitions[0].Width = new(m.RootGridCol0Width);
_modelListGrid.RowDefinitions[0].Height = new(m.ModelListRow0Height);
_explorerGrid.RowDefinitions[0].Height = new(m.ExplorerGridRow0Height);
_rightPanelGrid.RowDefinitions[0].Height = new(m.RightPanelGridRow0Height);
_vm.SFMLRendererViewModel.SetResolution(m.ResolutionX, m.ResolutionY);
_vm.SFMLRendererViewModel.MaxFps = m.MaxFps;
_vm.SFMLRendererViewModel.Speed = m.Speed;
_vm.SFMLRendererViewModel.ShowAxis = m.ShowAxis;
_vm.SFMLRendererViewModel.BackgroundColor = m.BackgroundColor;
}
}
private void SaveLastState()
{
var m = new LastStateModel()
{
WindowLeft = Left,
WindowTop = Top,
WindowWidth = Width,
WindowHeight = Height,
WindowState = WindowState,
RootGridCol0Width = _rootGrid.ColumnDefinitions[0].ActualWidth,
ModelListRow0Height = _modelListGrid.RowDefinitions[0].ActualHeight,
ExplorerGridRow0Height = _explorerGrid.RowDefinitions[0].ActualHeight,
RightPanelGridRow0Height = _rightPanelGrid.RowDefinitions[0].ActualHeight,
ResolutionX = _vm.SFMLRendererViewModel.ResolutionX,
ResolutionY = _vm.SFMLRendererViewModel.ResolutionY,
MaxFps = _vm.SFMLRendererViewModel.MaxFps,
Speed = _vm.SFMLRendererViewModel.Speed,
ShowAxis = _vm.SFMLRendererViewModel.ShowAxis,
BackgroundColor = _vm.SFMLRendererViewModel.BackgroundColor,
};
JsonHelper.Serialize(m, LastStateFilePath);
}
#region _spinesListView
private void SpinesListView_RequestSelectionChanging(object? sender, NotifyCollectionChangedEventArgs e)
@@ -125,6 +212,9 @@ public partial class MainWindow : Window
default:
break;
}
// 如果选中项发生变化也强制转移焦点
_spinesListView.Focus();
}
private void SpinesListView_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
@@ -234,11 +324,9 @@ public partial class MainWindow : Window
if (_fullScreenLayout.Visibility == Visibility.Visible) return;
IntPtr hwnd = new WindowInteropHelper(this).Handle;
if (Win32.GetScreenResolution(hwnd, out var resX, out var resY))
if (User32.GetScreenResolution(hwnd, out var resX, out var resY))
{
var vm = _vm.SFMLRendererViewModel;
vm.ResolutionX = resX;
vm.ResolutionY = resY;
_vm.SFMLRendererViewModel.SetResolution(resX, resY);
}
HandyControl.Controls.IconElement.SetGeometry(_fullScreenButton, AppResource.Geo_ArrowsMinimize);

View File

@@ -143,13 +143,17 @@
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_RenderSelectedOnly}"/>
<ToggleButton Grid.Row="0" Grid.Column="1" IsChecked="{Binding RenderSelectedOnly}"/>
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_Language}"/>
<ComboBox Grid.Row="1" Grid.Column="1"
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_AssociateFileSuffix}"/>
<ToggleButton Grid.Row="1" Grid.Column="1" IsChecked="{Binding AssociateFileSuffix}"/>
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_Language}"/>
<ComboBox Grid.Row="2" Grid.Column="1"
SelectedItem="{Binding AppLanguage}"
ItemsSource="{x:Static vm:PreferenceViewModel.AppLanguageOptions}"/>