Compare commits

...

72 Commits

Author SHA1 Message Date
ww-rm
34f9eeff2c Merge pull request #109 from ww-rm/dev/wpf
v0.16.0
2025-09-30 11:49:45 +08:00
ww-rm
1278fefea2 update readme 2025-09-30 11:47:55 +08:00
ww-rm
48d46afcff update readme 2025-09-30 11:12:06 +08:00
ww-rm
6742dacaf2 update to v0.16.0 2025-09-30 10:53:06 +08:00
ww-rm
3337ecc03a update changelog 2025-09-30 10:52:10 +08:00
ww-rm
b9eaacd1f7 修复可能的3.4版本附件残留问题 2025-09-30 10:30:25 +08:00
ww-rm
a0ada51325 修复跨线程错误 2025-09-30 09:21:54 +08:00
ww-rm
8e03911957 修复可能的窗口大小不正确问题 2025-09-30 08:55:00 +08:00
ww-rm
0b3db0fd0d 增加IsShuttingDownFromTray标志位 2025-09-30 08:45:28 +08:00
ww-rm
bb2862ed4f 增加最小化至托盘图标功能 2025-09-30 01:53:14 +08:00
ww-rm
8c3be98b54 修复记忆状态中的长度单位错误 2025-09-30 00:28:05 +08:00
ww-rm
b76224c010 调整顺序 2025-09-30 00:00:32 +08:00
ww-rm
bd9f8d714a 增加开机自启功能和自启设置 2025-09-29 23:26:06 +08:00
ww-rm
6900968555 调整布局 2025-09-29 00:05:41 +08:00
ww-rm
741d334a92 切换桌面投影时自动设置预览分辨率为主屏幕分辨率 2025-09-28 22:20:48 +08:00
ww-rm
b583108afa 更新模板 2025-09-28 20:44:12 +08:00
ww-rm
f7ace4dfe9 Merge pull request #106 from ww-rm/dev/wpf
v0.15.19
2025-09-27 23:48:05 +08:00
ww-rm
7ce2bd5629 update to v0.15.19 2025-09-27 23:46:25 +08:00
ww-rm
41716df7b2 update changelog 2025-09-27 23:45:49 +08:00
ww-rm
fe9b9829e2 增加 wallpaper view 2025-09-27 23:43:54 +08:00
ww-rm
4365c2a008 small change 2025-09-27 23:41:31 +08:00
ww-rm
7896e072e7 移除LastState中背景图片的记忆 2025-09-27 21:24:15 +08:00
ww-rm
940397c673 增加Texture加载失败时的日志信息 2025-09-27 18:49:29 +08:00
ww-rm
d57ea781f0 修复由于可能存在问题的奇数长度顶点数组导致的数组越界问题 2025-09-27 18:48:54 +08:00
ww-rm
b74f2811a7 add SFMLRenderWindow 2025-09-27 18:20:06 +08:00
ww-rm
66223f952b 重载后自动选中列表模型 2025-09-24 23:54:05 +08:00
ww-rm
0443d5e3d5 Merge pull request #104 from ww-rm/dev/wpf
v0.15.18
2025-09-24 23:45:55 +08:00
ww-rm
0a0b6a08e9 update to v0.15.18 2025-09-24 23:44:52 +08:00
ww-rm
63af4a19e6 update readme 2025-09-24 23:44:43 +08:00
ww-rm
51f0446c18 update changelog 2025-09-24 23:43:16 +08:00
ww-rm
e965223284 增强支持的纹理图像格式 2025-09-24 23:00:21 +08:00
ww-rm
dbc15952cc 增加工作区背景图片参数 2025-09-22 23:30:02 +08:00
ww-rm
93b70509ec 增加日志输出 2025-09-22 23:27:23 +08:00
ww-rm
798883d4e0 增加背景图案选项 2025-09-22 23:23:01 +08:00
ww-rm
3e88e65bbd 增加托盘图标 2025-09-22 20:09:46 +08:00
ww-rm
0906817cd3 修复面板高度首次还原错误 2025-09-22 14:38:04 +08:00
ww-rm
37235fa7d0 修改预览图背景颜色为透明 2025-09-22 08:35:27 +08:00
ww-rm
72935d8f2b utf8 2025-09-21 22:08:16 +08:00
ww-rm
8a4095dae1 完善窗口日志显示 2025-09-21 22:06:00 +08:00
ww-rm
b59f228f2e refactor 2025-09-21 11:01:58 +08:00
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
66 changed files with 2583 additions and 992 deletions

View File

@@ -15,4 +15,4 @@ assignees: ''
如果有必要,提供报错时的有关截图。/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.
请将会**出现问题的文件**以及**日志文件**打包成一个 ZIP 后作为附件贴在 issue 内,日志文件位于程序目录下的 `logs` 文件夹内。/Please compress the problematic files and the log files into a single ZIP archive and attach it to this issue. The log files are located in the `logs` folder under the program directory.

View File

@@ -1,5 +1,58 @@
# CHANGELOG
## v0.16.0
- 增加最小化至托盘图标功能
- 调整部分参数项的顺序
- 增加开机自启和自启文件设置
- 切换桌面投影时自动设置预览分辨率为主屏幕分辨率
- 修复 3.4 版本下可能存在的附件残留问题
## v0.15.19
- 模型重载后选中最后一个重载模型
- 修复 3.4 版本可能的奇数顶点数组导致的越界崩溃问题
- 移除参数自动记录中的背景图片路径
- 增加测试性桌面投影功能
## v0.15.18
- 完善窗口日志颜色标记
- 修复预览图背景颜色为透明
- 修复面板高度首次还原错误
- 增加托盘图标
- 增加可选预览背景画面和填充模式
- 增强支持的纹理格式(例如 webp
## v0.15.17
- 修改图标配色
## v0.15.16
- 修改模型添加顺序, 每次向顶层添加
- 添加模型后自动选中最近添加的模型S
- 点击预览画面或者选中项发生变化时转移焦点至列表
- 增加移除全部菜单项
- 增加单例模式和命令行文件参数
- 增加文件关联设置
## v0.15.15
- 增加报错信息
- 导入后自动选中最后一项
## v0.15.14
- 将预览画面的首选项移动至上一次状态参数中
- 增加预览画面像素的自动保存和恢复
- 增加日志启动时的版本号输出
## v0.15.13
- 增加程序布局自动存储和还原
- 增加部分预览画面首选项
## v0.15.12
- 增加单个模型和单个轨道的时间因子

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
@@ -7,7 +7,7 @@
<TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.15.4</Version>
<Version>0.16.0</Version>
<UseWPF>true</UseWPF>
</PropertyGroup>

View File

@@ -1,92 +1,53 @@
//
// Copyright (c) 2004-2011 Jaroslaw Kowalski <jaak@jkowalski.net>
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// * Neither the name of Jaroslaw Kowalski nor the names of its
// contributors may be used to endorse or promote products derived from this
// software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
//
using NLog;
using NLog.Conditions;
using NLog.Config;
using NLog;
using System.ComponentModel;
using NLog.Layouts;
using System.Windows;
namespace NLog.Windows.Wpf
{
[NLogConfigurationItem]
public class RichTextBoxRowColoringRule
{
static RichTextBoxRowColoringRule()
{
Default = new RichTextBoxRowColoringRule();
}
public RichTextBoxRowColoringRule()
: this(null, "Empty", "Empty", FontStyles.Normal, FontWeights.Normal)
{
}
public RichTextBoxRowColoringRule(string condition, string fontColor, string backColor, FontStyle fontStyle, FontWeight fontWeight)
{
Condition = condition;
FontColor = fontColor;
BackgroundColor = backColor;
Style = fontStyle;
Weight = fontWeight;
}
public RichTextBoxRowColoringRule(string condition, string fontColor, string backColor)
{
Condition = condition;
FontColor = fontColor;
BackgroundColor = backColor;
Style = FontStyles.Normal;
Weight = FontWeights.Normal;
}
public static RichTextBoxRowColoringRule Default { get; private set; }
[RequiredParameter]
public ConditionExpression Condition { get; set; }
[DefaultValue("Empty")]
public string FontColor { get; set; }
public Layout FontColor { get; set; }
public Layout BackgroundColor { get; set; }
[DefaultValue("Empty")]
public string BackgroundColor { get; set; }
public FontStyle FontStyle { get; set; }
public FontWeight FontWeight { get; set; }
public FontStyle Style { get; set; }
static RichTextBoxRowColoringRule()
{
RichTextBoxRowColoringRule.Default = new RichTextBoxRowColoringRule();
}
public FontWeight Weight { get; set; }
public RichTextBoxRowColoringRule() : this(null, "Empty", "Empty", FontStyles.Normal, FontWeights.Normal) { }
public RichTextBoxRowColoringRule(string condition, string fontColor, string backColor)
{
this.Condition = (ConditionExpression)condition;
this.FontColor = Layout.FromString(fontColor);
this.BackgroundColor = Layout.FromString(backColor);
this.FontStyle = FontStyles.Normal;
this.FontWeight = FontWeights.Normal;
}
public RichTextBoxRowColoringRule(string condition, string fontColor, string backColor, FontStyle fontStyle, FontWeight fontWeight)
{
this.Condition = (ConditionExpression)condition;
this.FontColor = Layout.FromString(fontColor);
this.BackgroundColor = Layout.FromString(backColor);
this.FontStyle = fontStyle;
this.FontWeight = fontWeight;
}
public bool CheckCondition(LogEventInfo logEvent)
{
return true.Equals(Condition.Evaluate(logEvent));
return true.Equals(this.Condition.Evaluate(logEvent));
}
}
}

View File

@@ -1,34 +1,27 @@
using NLog.Config;
using NLog.Layouts;
using NLog;
using NLog.Common;
using NLog.Config;
using NLog.Targets;
using NLog;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows;
namespace NLog.Windows.Wpf
{
// TODO: 完善日志实现
[Target("RichTextBox")]
public sealed class RichTextBoxTarget : TargetWithLayout
{
private int lineCount;
private int _width = 500;
private int _height = 500;
private static readonly TypeConverter colorConverter = new ColorConverter();
public static ReadOnlyCollection<RichTextBoxRowColoringRule> DefaultRowColoringRules { get; } = CreateDefaultColoringRules();
static RichTextBoxTarget()
private static ReadOnlyCollection<RichTextBoxRowColoringRule> CreateDefaultColoringRules()
{
var rules = new List<RichTextBoxRowColoringRule>()
return new List<RichTextBoxRowColoringRule>()
{
new RichTextBoxRowColoringRule("level == LogLevel.Fatal", "White", "Red", FontStyles.Normal, FontWeights.Bold),
new RichTextBoxRowColoringRule("level == LogLevel.Error", "Red", "Empty", FontStyles.Italic, FontWeights.Bold),
@@ -36,221 +29,253 @@ namespace NLog.Windows.Wpf
new RichTextBoxRowColoringRule("level == LogLevel.Info", "Black", "Empty"),
new RichTextBoxRowColoringRule("level == LogLevel.Debug", "Gray", "Empty"),
new RichTextBoxRowColoringRule("level == LogLevel.Trace", "DarkGray", "Empty", FontStyles.Italic, FontWeights.Normal),
};
DefaultRowColoringRules = rules.AsReadOnly();
}.AsReadOnly();
}
public RichTextBoxTarget()
{
WordColoringRules = new List<RichTextBoxWordColoringRule>();
RowColoringRules = new List<RichTextBoxRowColoringRule>();
ToolWindow = true;
}
private delegate void DelSendTheMessageToRichTextBox(string logMessage, RichTextBoxRowColoringRule rule);
private delegate void FormCloseDelegate();
public static ReadOnlyCollection<RichTextBoxRowColoringRule> DefaultRowColoringRules { get; private set; }
public RichTextBoxTarget() { }
public string ControlName { get; set; }
public string FormName { get; set; }
public string WindowName { get; set; }
[DefaultValue(false)]
public bool UseDefaultRowColoringRules { get; set; }
[ArrayParameter(typeof(RichTextBoxRowColoringRule), "row-coloring")]
public IList<RichTextBoxRowColoringRule> RowColoringRules { get; private set; }
[ArrayParameter(typeof(RichTextBoxWordColoringRule), "word-coloring")]
public IList<RichTextBoxWordColoringRule> WordColoringRules { get; private set; }
[DefaultValue(true)]
public bool ToolWindow { get; set; }
public bool ShowMinimized { get; set; }
public int Width
{
get { return _width; }
set { _width = value; }
}
public int Height
{
get { return _height; }
set { _height = value; }
}
public bool AutoScroll { get; set; }
public int MaxLines { get; set; }
internal Window TargetForm { get; set; }
[ArrayParameter(typeof(RichTextBoxRowColoringRule), "row-coloring")]
public IList<RichTextBoxRowColoringRule> RowColoringRules { get; } = new List<RichTextBoxRowColoringRule>();
internal RichTextBox TargetRichTextBox { get; set; }
[ArrayParameter(typeof(RichTextBoxWordColoringRule), "word-coloring")]
public IList<RichTextBoxWordColoringRule> WordColoringRules { get; } = new List<RichTextBoxWordColoringRule>();
internal bool CreatedForm { get; set; }
[NLogConfigurationIgnoreProperty]
public Window TargetWindow { get; set; }
[NLogConfigurationIgnoreProperty]
public RichTextBox TargetRichTextBox { get; set; }
protected override void InitializeTarget()
{
TargetRichTextBox = Application.Current.MainWindow.FindName(ControlName) as RichTextBox;
base.InitializeTarget();
if (TargetRichTextBox != null)
return;
if (TargetRichTextBox != null) return;
//this.TargetForm = FormHelper.CreateForm(this.FormName, this.Width, this.Height, false, this.ShowMinimized, this.ToolWindow);
//this.CreatedForm = true;
var openFormByName = Application.Current.Windows.Cast<Window>().FirstOrDefault(x => x.GetType().Name == FormName);
if (openFormByName != null)
if (WindowName == null)
{
TargetForm = openFormByName;
if (string.IsNullOrEmpty(ControlName))
{
// throw new NLogConfigurationException("Rich text box control name must be specified for " + GetType().Name + ".");
Trace.WriteLine("Rich text box control name must be specified for " + GetType().Name + ".");
}
CreatedForm = false;
TargetRichTextBox = TargetForm.FindName(ControlName) as RichTextBox;
if (TargetRichTextBox == null)
{
// throw new NLogConfigurationException("Rich text box control '" + ControlName + "' cannot be found on form '" + FormName + "'.");
Trace.WriteLine("Rich text box control '" + ControlName + "' cannot be found on form '" + FormName + "'.");
}
HandleError("WindowName should be specified for {0}.{1}", GetType().Name, Name);
return;
}
if (TargetRichTextBox == null)
if (string.IsNullOrEmpty(ControlName))
{
TargetForm = new Window
{
Name = FormName,
Width = Width,
Height = Height,
WindowStyle = ToolWindow ? WindowStyle.ToolWindow : WindowStyle.None,
WindowState = ShowMinimized ? WindowState.Minimized : WindowState.Normal,
Title = "NLog Messages"
};
TargetForm.Show();
TargetRichTextBox = new RichTextBox { Name = ControlName };
var style = new Style(typeof(Paragraph));
TargetRichTextBox.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
style.Setters.Add(new Setter(Block.MarginProperty, new Thickness(0, 0, 0, 0)));
TargetRichTextBox.Resources.Add(typeof(Paragraph), style);
TargetForm.Content = TargetRichTextBox;
CreatedForm = true;
HandleError("Rich text box control name must be specified for {0}.{1}", GetType().Name, Name);
return;
}
var targetWindow = Application.Current.Windows.OfType<Window>().FirstOrDefault(w => w.Name == WindowName);
if (targetWindow == null)
{
InternalLogger.Info("{0}: WindowName '{1}' not found", this, WindowName);
return;
}
var targetControl = targetWindow.FindName(ControlName) as RichTextBox;
if (targetControl == null)
{
InternalLogger.Info("{0}: WIndowName '{1}' does not contain ControlName '{2}'", this, WindowName, ControlName);
return;
}
AttachToControl(targetWindow, targetControl);
}
private static void HandleError(string message, params object[] args)
{
if (LogManager.ThrowExceptions)
{
throw new NLogConfigurationException(string.Format(message, args));
}
InternalLogger.Error(message, args);
}
private void AttachToControl(Window window, RichTextBox textboxControl)
{
InternalLogger.Info("{0}: Attaching target to textbox {1}.{2}", this, window.Name, textboxControl.Name);
DetachFromControl();
TargetWindow = window;
TargetRichTextBox = textboxControl;
}
private void DetachFromControl()
{
TargetWindow = null;
TargetRichTextBox = null;
}
protected override void CloseTarget()
{
if (CreatedForm)
{
try
{
TargetForm.Dispatcher.Invoke(() =>
{
TargetForm.Close();
TargetForm = null;
});
}
catch
{
}
}
DetachFromControl();
}
protected override void Write(LogEventInfo logEvent)
{
RichTextBoxRowColoringRule matchingRule = RowColoringRules.FirstOrDefault(rr => rr.CheckCondition(logEvent));
if (UseDefaultRowColoringRules && matchingRule == null)
RichTextBox textbox = TargetRichTextBox;
if (textbox == null || textbox.Dispatcher.HasShutdownStarted || textbox.Dispatcher.HasShutdownFinished)
{
foreach (var rr in DefaultRowColoringRules.Where(rr => rr.CheckCondition(logEvent)))
{
matchingRule = rr;
break;
}
//no last logged textbox
InternalLogger.Trace("{0}: Attached Textbox is {1}, skipping logging", this, textbox == null ? "null" : "disposed");
return;
}
if (matchingRule == null)
{
matchingRule = RichTextBoxRowColoringRule.Default;
}
var logMessage = Layout.Render(logEvent);
if (Application.Current == null) return;
string logMessage = RenderLogEvent(Layout, logEvent);
RichTextBoxRowColoringRule matchingRule = FindMatchingRule(logEvent);
_ = DoSendMessageToTextbox(logMessage, matchingRule, logEvent);
}
private bool DoSendMessageToTextbox(string logMessage, RichTextBoxRowColoringRule rule, LogEventInfo logEvent)
{
RichTextBox textbox = TargetRichTextBox;
try
{
if (Application.Current.Dispatcher.CheckAccess() == false)
if (textbox != null && !textbox.Dispatcher.HasShutdownStarted && !textbox.Dispatcher.HasShutdownFinished)
{
Application.Current.Dispatcher.Invoke(() => SendTheMessageToRichTextBox(logMessage, matchingRule));
}
else
{
SendTheMessageToRichTextBox(logMessage, matchingRule);
if (!textbox.Dispatcher.CheckAccess())
{
textbox.Dispatcher.BeginInvoke(() => SendTheMessageToRichTextBox(textbox, logMessage, rule, logEvent));
}
else
{
SendTheMessageToRichTextBox(textbox, logMessage, rule, logEvent);
}
return true;
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
InternalLogger.Warn(ex, "{0}: Failed to append RichTextBox", this);
}
private static Color GetColorFromString(string color, Brush defaultColor)
{
if (color == "Empty")
{
return defaultColor is SolidColorBrush solidBrush ? solidBrush.Color : Colors.White;
}
return (Color)colorConverter.ConvertFromString(color);
}
private void SendTheMessageToRichTextBox(string logMessage, RichTextBoxRowColoringRule rule)
{
RichTextBox rtbx = TargetRichTextBox;
var tr = new TextRange(rtbx.Document.ContentEnd, rtbx.Document.ContentEnd);
tr.Text = logMessage + "\n";
tr.ApplyPropertyValue(TextElement.ForegroundProperty,
new SolidColorBrush(GetColorFromString(rule.FontColor, (Brush)tr.GetPropertyValue(TextElement.ForegroundProperty)))
);
tr.ApplyPropertyValue(TextElement.BackgroundProperty,
new SolidColorBrush(GetColorFromString(rule.BackgroundColor, (Brush)tr.GetPropertyValue(TextElement.BackgroundProperty)))
);
tr.ApplyPropertyValue(TextElement.FontStyleProperty, rule.Style);
tr.ApplyPropertyValue(TextElement.FontWeightProperty, rule.Weight);
if (MaxLines > 0)
{
lineCount++;
if (lineCount > MaxLines)
if (LogManager.ThrowExceptions)
{
tr = new TextRange(rtbx.Document.ContentStart, rtbx.Document.ContentEnd);
tr.Text.Remove(0, tr.Text.IndexOf('\n'));
lineCount--;
throw;
}
}
return false;
}
private RichTextBoxRowColoringRule FindMatchingRule(LogEventInfo logEvent)
{
//custom rules first
if (RowColoringRules.Count > 0)
{
foreach (RichTextBoxRowColoringRule coloringRule in RowColoringRules)
{
if (coloringRule.CheckCondition(logEvent))
{
return coloringRule;
}
}
}
if (UseDefaultRowColoringRules && DefaultRowColoringRules != null)
{
foreach (RichTextBoxRowColoringRule coloringRule in DefaultRowColoringRules)
{
if (coloringRule.CheckCondition(logEvent))
{
return coloringRule;
}
}
}
return RichTextBoxRowColoringRule.Default;
}
private void SendTheMessageToRichTextBox(RichTextBox textBox, string logMessage, RichTextBoxRowColoringRule rule, LogEventInfo logEvent)
{
if (textBox == null) return;
var document = textBox.Document;
// 插入文本(带换行)
var tr = new TextRange(document.ContentEnd, document.ContentEnd)
{
Text = logMessage + Environment.NewLine
};
// 设置行级样式
var fgColor = rule.FontColor?.Render(logEvent);
var bgColor = rule.BackgroundColor?.Render(logEvent);
tr.ApplyPropertyValue(TextElement.ForegroundProperty,
string.IsNullOrEmpty(fgColor) || fgColor == "Empty"
? textBox.Foreground
: new SolidColorBrush((Color)ColorConverter.ConvertFromString(fgColor)));
tr.ApplyPropertyValue(TextElement.BackgroundProperty,
string.IsNullOrEmpty(bgColor) || bgColor == "Empty"
? Brushes.Transparent
: new SolidColorBrush((Color)ColorConverter.ConvertFromString(bgColor)));
tr.ApplyPropertyValue(TextElement.FontStyleProperty, rule.FontStyle);
tr.ApplyPropertyValue(TextElement.FontWeightProperty, rule.FontWeight);
// Word coloring在刚插入的范围内做匹配
if (WordColoringRules.Count > 0)
{
foreach (var wordRule in WordColoringRules)
{
var pattern = wordRule.Regex?.Render(logEvent) ?? string.Empty;
var text = wordRule.Text?.Render(logEvent) ?? string.Empty;
var wholeWords = wordRule.WholeWords.RenderValue(logEvent);
var ignoreCase = wordRule.IgnoreCase.RenderValue(logEvent);
var regex = wordRule.ResolveRegEx(pattern, text, wholeWords, ignoreCase);
var matches = regex.Matches(tr.Text);
foreach (Match match in matches)
{
// 匹配到的部分范围
var start = tr.Start.GetPositionAtOffset(match.Index, LogicalDirection.Forward);
var endPos = tr.Start.GetPositionAtOffset(match.Index + match.Length, LogicalDirection.Backward);
if (start == null || endPos == null) continue;
var wordRange = new TextRange(start, endPos);
var wordFg = wordRule.FontColor?.Render(logEvent);
var wordBg = wordRule.BackgroundColor?.Render(logEvent);
wordRange.ApplyPropertyValue(TextElement.ForegroundProperty,
string.IsNullOrEmpty(wordFg) || wordFg == "Empty"
? tr.GetPropertyValue(TextElement.ForegroundProperty)
: new SolidColorBrush((Color)ColorConverter.ConvertFromString(wordFg)));
wordRange.ApplyPropertyValue(TextElement.BackgroundProperty,
string.IsNullOrEmpty(wordBg) || wordBg == "Empty"
? tr.GetPropertyValue(TextElement.BackgroundProperty)
: new SolidColorBrush((Color)ColorConverter.ConvertFromString(wordBg)));
wordRange.ApplyPropertyValue(TextElement.FontStyleProperty, wordRule.FontStyle);
wordRange.ApplyPropertyValue(TextElement.FontWeightProperty, wordRule.FontWeight);
}
}
}
// 限制最大行数
if (MaxLines > 0)
{
while (document.Blocks.Count > MaxLines)
{
document.Blocks.Remove(document.Blocks.FirstBlock);
}
}
// 自动滚动到最后
if (AutoScroll)
{
rtbx.ScrollToEnd();
textBox.ScrollToEnd();
}
}
}
}
}

View File

@@ -1,119 +1,59 @@
//
// Copyright (c) 2004-2011 Jaroslaw Kowalski <jaak@jkowalski.net>
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// * Neither the name of Jaroslaw Kowalski nor the names of its
// contributors may be used to endorse or promote products derived from this
// software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
//
using NLog.Config;
using NLog.Layouts;
using System.ComponentModel;
using System.Text.RegularExpressions;
using System.Windows;
using NLog.Config;
namespace NLog.Windows.Wpf
{
[NLogConfigurationItem]
[NLogConfigurationItem]
public class RichTextBoxWordColoringRule
{
private Regex compiledRegex;
public Layout Regex { get; set; }
public Layout Text { get; set; }
public Layout<bool> WholeWords { get; set; }
public Layout<bool> IgnoreCase { get; set; }
public RichTextBoxWordColoringRule()
public Layout FontColor { get; set; }
public Layout BackgroundColor { get; set; }
public FontStyle FontStyle { get; set; }
public FontWeight FontWeight { get; set; }
internal Regex ResolveRegEx(string pattern, string text, bool wholeWords, bool ignoreCase)
{
FontColor = "Empty";
BackgroundColor = "Empty";
if (string.IsNullOrEmpty(pattern) && text != null)
{
pattern = System.Text.RegularExpressions.Regex.Escape(text);
if (wholeWords)
pattern = "\b" + pattern + "\b";
}
RegexOptions options = RegexOptions.None;
if (ignoreCase)
options |= RegexOptions.IgnoreCase;
return new Regex(pattern, options); // RegEx-Cache
}
public RichTextBoxWordColoringRule() : this(null, "Empty", "Empty", FontStyles.Normal, FontWeights.Normal) { }
public RichTextBoxWordColoringRule(string text, string fontColor, string backgroundColor)
{
Text = text;
FontColor = fontColor;
BackgroundColor = backgroundColor;
Style = FontStyles.Normal;
Weight = FontWeights.Normal;
this.Text = text;
this.FontColor = Layout.FromString(fontColor);
this.BackgroundColor = Layout.FromString(backgroundColor);
this.FontStyle = FontStyles.Normal;
this.FontWeight = FontWeights.Normal;
}
public RichTextBoxWordColoringRule(string text, string textColor, string backgroundColor, FontStyle fontStyle, FontWeight fontWeight)
{
Text = text;
FontColor = textColor;
BackgroundColor = backgroundColor;
Style = fontStyle;
Weight = fontWeight;
this.Text = text;
this.FontColor = Layout.FromString(textColor);
this.BackgroundColor = Layout.FromString(backgroundColor);
this.FontStyle = fontStyle;
this.FontWeight = fontWeight;
}
public string Regex { get; set; }
public string Text { get; set; }
[DefaultValue(false)]
public bool WholeWords { get; set; }
[DefaultValue(false)]
public bool IgnoreCase { get; set; }
public FontStyle Style { get; set; }
public FontWeight Weight { get; set; }
public Regex CompiledRegex
{
get
{
if (compiledRegex == null)
{
string regexpression = Regex;
if (regexpression == null && Text != null)
{
regexpression = System.Text.RegularExpressions.Regex.Escape(Text);
if (WholeWords)
{
regexpression = "\b" + regexpression + "\b";
}
}
RegexOptions regexOptions = RegexOptions.Compiled;
if (IgnoreCase)
{
regexOptions |= RegexOptions.IgnoreCase;
}
compiledRegex = new Regex(regexpression, regexOptions);
}
return compiledRegex;
}
}
[DefaultValue("Empty")]
public string FontColor { get; set; }
[DefaultValue("Empty")]
public string BackgroundColor { get; set; }
}
}

View File

@@ -8,27 +8,32 @@
A simple and user-friendly Spine file viewer and exporter with multi-language support (Chinese/English/Japanese).
![previewer](img/preview.webp)
![previewer](https://github.com/user-attachments/assets/697ae86f-ddf0-445d-951c-cf04f5206e40)
<video src="https://github.com/user-attachments/assets/37b6b730-088a-4352-827a-c338127a16f0">
## Features
* Supports multiple versions of Spine files.
* Batch open files via drag-and-drop or copy-paste.
* Batch preview functionality.
* List-based multi-skeleton viewing and render order management.
* Batch adjustment of skeleton parameters using multi-selection.
* Multi-track animation settings.
* 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.
* ...
- Multiple versions of Spine files
- Batch file opening via drag-and-drop or copy-paste
- Batch preview
- List-based multi-skeleton viewing and render order management
- Multi-selection in lists for batch skeleton parameter settings
- Multi-track animation settings
- Skin and custom slot attachment settings
- Custom slot visibility
- Debug rendering
- Playback speed adjustment for view/model/track timelines
- Track alpha blending parameter settings
- Fullscreen preview
- Export to single frame, image sequence, animated GIF, or video file
- Automatic resolution batch export
- Custom export with FFmpeg
- Program parameter saving
- File extension association
- Texture images in formats other than PNG
- Launch at startup with persistent dynamic wallpaper
- ......
### Supported Spine Versions
@@ -76,14 +81,14 @@ In the menu, go to "File" -> "Preferences..." -> "Language," select your desired
The program is organized into a left-right layout:
* **Left Panel:** Functionality panel.
* **Right Panel:** Preview display.
- **Left Panel:** Functionality panel.
- **Right Panel:** Preview display.
The left panel includes three sub-panels:
* **Browse:** Preview the content of a specified folder without importing files into the program. This panel allows generating `.webp` previews for models or importing selected models.
* **Model:** Lists imported models for rendering. Parameters and rendering order can be adjusted here, along with other model-related functionalities.
* **Display:** Adjust parameters for the right-side preview display.
- **Browse:** Preview the content of a specified folder without importing files into the program. This panel allows generating `.webp` previews for models or importing selected models.
- **Model:** Lists imported models for rendering. Parameters and rendering order can be adjusted here, along with other model-related functionalities.
- **Display:** Adjust parameters for the right-side preview display.
Hover your mouse over buttons, labels, or input fields to see help text for most UI elements.
@@ -99,10 +104,10 @@ The Model panel supports right-click menus, some shortcuts, and batch adjustment
For preview display adjustments:
* **Left-click:** Select and drag models. Hold `Ctrl` for multi-selection, synchronized with the left-side list.
* **Right-click:** Drag the entire display.
* **Scroll wheel:** Zoom in/out. Hold `Ctrl` to scale selected models.
* **Render selected-only mode:** In this mode, the preview only shows selected models, and selection status can only be changed via the left-side list.
- **Left-click:** Select and drag models. Hold `Ctrl` for multi-selection, synchronized with the left-side list.
- **Right-click:** Drag the entire display.
- **Scroll wheel:** Zoom in/out. Hold `Ctrl` to scale selected models.
- **Render selected-only mode:** In this mode, the preview only shows selected models, and selection status can only be changed via the left-side list.
The buttons below the preview display allow time adjustments, serving as a simple playback control.
@@ -114,9 +119,17 @@ Use the right-click menu in the Model panel to export selected items.
Key export parameters include:
* **Output folder:** Optional. When not specified, output is saved to the respective model folder; otherwise, all output is saved to the provided folder.
* **Export single:** By default, each model is exported independently. Selecting "Export single" renders all selected models in a single frame, producing a unified output.
* **Auto resolution:** Ignores the preview resolution and viewport parameters, exporting output at the actual size of the content. For animations/videos, the output matches the size required for full visibility.
- **Output folder:** Optional. When not specified, output is saved to the respective model folder; otherwise, all output is saved to the provided folder.
- **Export single:** By default, each model is exported independently. Selecting "Export single" renders all selected models in a single frame, producing a unified output.
- **Auto resolution:** Ignores the preview resolution and viewport parameters, exporting output at the actual size of the content. For animations/videos, the output matches the size required for full visibility.
### Dynamic Wallpaper
Dynamic wallpaper is implemented through desktop projection, allowing the content of the current preview to be projected onto the desktop in real time.
You can enable or disable desktop projection from the program preferences or the right-click menu of the tray icon. After adjusting the model and display parameters, you can save the current configuration as a workspace file for convenient restoration later.
If you want the wallpaper to stay active after startup, you can enable auto-start in the preferences and specify which workspace file should be loaded when the program launches.
### More Information
@@ -124,12 +137,12 @@ For detailed usage and documentation, see the [Wiki](https://github.com/ww-rm/Sp
## Acknowledgements
* [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes)
* [SFML.Net](https://github.com/SFML/SFML.Net)
* [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore)
* [HandyControl](https://github.com/HandyOrg/HandyControl)
* [NLog](https://github.com/NLog/NLog)
* [SkiaSharp](https://github.com/mono/SkiaSharp)
- [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes)
- [SFML.Net](https://github.com/SFML/SFML.Net)
- [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore)
- [HandyControl](https://github.com/HandyOrg/HandyControl)
- [NLog](https://github.com/NLog/NLog)
- [SkiaSharp](https://github.com/mono/SkiaSharp)
---

View File

@@ -6,9 +6,11 @@
[中文](README.md) | [English](README.en.md)
一个简单好用的 Spine 文件查看&导出程序, 支持中/英/日多语言界面.
Spine 文件查看&导出程序, 同时也是支持 Spine 的动态壁纸程序.
![previewer](img/preview.webp)
![previewer](https://github.com/user-attachments/assets/697ae86f-ddf0-445d-951c-cf04f5206e40)
<video src="https://github.com/user-attachments/assets/37b6b730-088a-4352-827a-c338127a16f0">
## 功能
@@ -28,6 +30,9 @@
- 支持自动分辨率批量导出
- 支持 FFmpeg 自定义导出
- 支持程序参数保存
- 支持文件后缀关联
- 支持非 png 格式的纹理图片格式
- 支持开机自启常驻动态壁纸
- ......
### Spine 版本支持
@@ -115,6 +120,14 @@
- 导出单个. 默认是每个模型独立导出, 即对模型列表进行批量操作, 如果选择仅导出单个, 那么被导出的所有模型将在同一个画面上被渲染, 输出产物只有一份.
- 自动分辨率. 该模式会忽略预览画面的分辨率和视区参数, 导出产物的分辨率与被导出内容的实际大小一致, 如果是动图或者视频则会与完整显示动画的必需大小一致.
### 动态壁纸
动态壁纸通过桌面投影实现, 可以将当前预览画面上的内容实时投影至桌面.
在程序首选项或者托盘图标右键菜单中可以进行桌面投影的启用与否, 模型和画面参数调整完成后, 可以将当前参数保存为工作区文件, 方便之后恢复该配置.
如果希望开机自启常驻壁纸, 也可以在首选项中启用开机自启, 并且设置启动后需要加载的工作区文件.
### 更多
更为详细的使用方法和说明见 [Wiki](https://github.com/ww-rm/SpineViewer/wiki), 有使用上的问题或者 BUG 可以提个 [Issue](https://github.com/ww-rm/SpineViewer/issues).

View File

@@ -64,10 +64,10 @@ namespace SFMLRenderer
hs?.Dispose();
}
private nint HwndMessageHook(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled)
private IntPtr HwndMessageHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
_renderWindow?.DispatchEvents();
return nint.Zero;
return IntPtr.Zero;
}
}
}

View File

@@ -0,0 +1,171 @@
using SFML.Graphics;
using SFML.System;
using SFML.Window;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Threading;
namespace SFMLRenderer
{
public class SFMLRenderWindow : RenderWindow, ISFMLRenderer
{
private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(10) };
public SFMLRenderWindow(VideoMode mode, string title, Styles style) : base(mode, title, style)
{
SetActive(false);
_timer.Tick += (s, e) => DispatchEvents();
_timer.Start();
RendererCreated?.Invoke(this, EventArgs.Empty);
}
public event EventHandler? RendererCreated;
public event EventHandler? RendererDisposing
{
add => throw new NotImplementedException();
remove => throw new NotImplementedException();
}
public event EventHandler<MouseMoveEventArgs>? CanvasMouseMove
{
add { MouseMoved += value; }
remove { MouseMoved -= value; }
}
public event EventHandler<MouseButtonEventArgs>? CanvasMouseButtonPressed
{
add { MouseButtonPressed += value; }
remove { MouseButtonPressed -= value; }
}
public event EventHandler<MouseButtonEventArgs>? CanvasMouseButtonReleased
{
add { MouseButtonReleased += value; }
remove { MouseButtonReleased -= value; }
}
public event EventHandler<MouseWheelScrollEventArgs>? CanvasMouseWheelScrolled
{
add { MouseWheelScrolled += value; }
remove { MouseWheelScrolled -= value; }
}
public Vector2u Resolution
{
get => Size;
set => Size = value;
}
public Vector2f Center
{
get
{
using var view = GetView();
return view.Center;
}
set
{
using var view = GetView();
view.Center = value;
SetView(view);
}
}
public float Zoom
{
get
{
using var view = GetView();
return Math.Abs(Size.X / view.Size.X); // XXX: 仅使用宽度进行缩放计算
}
set
{
value = Math.Abs(value);
if (value <= 0) return;
using var view = GetView();
var signX = Math.Sign(view.Size.X);
var signY = Math.Sign(view.Size.Y);
var resolution = Size;
view.Size = new(resolution.X / value * signX, resolution.Y / value * signY);
SetView(view);
}
}
public float Rotation
{
get
{
using var view = GetView();
return view.Rotation;
}
set
{
using var view = GetView();
view.Rotation = value;
SetView(view);
}
}
public bool FlipX
{
get
{
using var view = GetView();
return view.Size.X < 0;
}
set
{
using var view = GetView();
var size = view.Size;
if (size.X > 0 && value || size.X < 0 && !value)
size.X *= -1;
view.Size = size;
SetView(view);
}
}
public bool FlipY
{
get
{
using var view = GetView();
return view.Size.Y < 0;
}
set
{
using var view = GetView();
var size = view.Size;
if (size.Y > 0 && value || size.Y < 0 && !value)
size.Y *= -1;
view.Size = size;
SetView(view);
}
}
public uint MaxFps
{
get => _maxFps;
set
{
SetFramerateLimit(value);
_maxFps = value;
}
}
private uint _maxFps = 0;
public bool VerticalSync
{
get => _verticalSync;
set
{
SetVerticalSyncEnabled(value);
_verticalSync = value;
}
}
private bool _verticalSync = false;
}
}

View File

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

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.12</Version>
<Version>0.16.0</Version>
</PropertyGroup>
<PropertyGroup>

View File

@@ -115,8 +115,9 @@ namespace Spine
{
_data = SpineObjectData.New(Version, skelPath, atlasPath, textureLoader);
}
catch
catch (Exception ex)
{
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load spine with version '{version}'");
}
}

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,4 +1,7 @@
using System;
using NLog;
using SFML.Graphics;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -20,6 +23,8 @@ namespace Spine.SpineWrappers
SpineRuntime41.TextureLoader,
SpineRuntime42.TextureLoader
{
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 默认的全局纹理加载器
/// </summary>
@@ -40,37 +45,38 @@ namespace Spine.SpineWrappers
/// </summary>
public bool ForceMipmap { get; set; }
private SFML.Graphics.Texture ReadTexture(string path)
private Texture ReadTexture(string path)
{
if (ForcePremul)
if (!File.Exists(path))
{
using var image = new SFML.Graphics.Image(path);
var width = image.Size.X;
var height = image.Size.Y;
var pixels = image.Pixels;
var size = width * height * 4;
for (int i = 0; i < size; i += 4)
{
byte a = pixels[i + 3];
if (a == 0)
{
pixels[i + 0] = 0;
pixels[i + 1] = 0;
pixels[i + 2] = 0;
}
else if (a != 255)
{
float f = a / 255f;
pixels[i + 0] = (byte)(pixels[i + 0] * f);
pixels[i + 1] = (byte)(pixels[i + 1] * f);
pixels[i + 2] = (byte)(pixels[i + 2] * f);
}
}
var tex = new SFML.Graphics.Texture(width, height);
tex.Update(pixels);
return tex;
_logger.Error($"Texture file not found, {path}");
throw new FileNotFoundException("Texture file not found", path);
}
return new(path);
using var codec = SKCodec.Create(path, out var result);
if (codec is null || result != SKCodecResult.Success)
{
_logger.Error($"Failed to create codec '{path}', {result}");
throw new InvalidOperationException($"Failed to create codec '{path}', {result}");
}
var width = codec.Info.Width;
var height = codec.Info.Height;
// 判断是否需要强制预乘
var alphaType = ForcePremul ? SKAlphaType.Premul : SKAlphaType.Unpremul;
var info = new SKImageInfo(width, height, SKColorType.Rgba8888, alphaType);
result = codec.GetPixels(info, out var pixels);
if (result != SKCodecResult.Success)
{
_logger.Error($"Failed to decode image '{path}', {result}");
throw new InvalidOperationException($"Failed to decode image '{path}', {result}");
}
Texture tex = new((uint)width, (uint)height);
tex.Update(pixels);
return tex;
}
public virtual void Load(SpineRuntime21.AtlasPage page, string path)
@@ -394,7 +400,7 @@ namespace Spine.SpineWrappers
public virtual void Unload(object texture)
{
((SFML.Graphics.Texture)texture).Dispose();
((Texture)texture).Dispose();
}
}
}

View File

@@ -338,7 +338,7 @@ namespace SpineRuntime21 {
if (vertices != null)
{
for (int ii = 0; ii < verticesLength; ii += 2)
for (int ii = 0; ii + 1 < verticesLength; ii += 2)
{
float vx = vertices[ii], vy = vertices[ii + 1];
minX = Math.Min(minX, vx);

View File

@@ -489,7 +489,7 @@ namespace SpineRuntime34 {
if (vertices != null)
{
for (int ii = 0; ii < verticesLength; ii += 2)
for (int ii = 0; ii + 1 < verticesLength; ii += 2)
{
float vx = vertices[ii], vy = vertices[ii + 1];
minX = Math.Min(minX, vx);

View File

@@ -521,7 +521,7 @@ namespace SpineRuntime35 {
}
if (vertices != null) {
for (int ii = 0; ii < verticesLength; ii += 2) {
for (int ii = 0; ii + 1 < verticesLength; ii += 2) {
float vx = vertices[ii], vy = vertices[ii + 1];
minX = Math.Min(minX, vx);
minY = Math.Min(minY, vy);

View File

@@ -528,7 +528,7 @@ namespace SpineRuntime36 {
}
if (vertices != null) {
for (int ii = 0; ii < verticesLength; ii += 2) {
for (int ii = 0; ii + 1 < verticesLength; ii += 2) {
float vx = vertices[ii], vy = vertices[ii + 1];
minX = Math.Min(minX, vx);
minY = Math.Min(minY, vy);

View File

@@ -580,7 +580,7 @@ namespace SpineRuntime37 {
}
if (vertices != null) {
for (int ii = 0; ii < verticesLength; ii += 2) {
for (int ii = 0; ii + 1 < verticesLength; ii += 2) {
float vx = vertices[ii], vy = vertices[ii + 1];
minX = Math.Min(minX, vx);
minY = Math.Min(minY, vy);

View File

@@ -617,7 +617,7 @@ namespace SpineRuntime38 {
}
if (vertices != null) {
for (int ii = 0; ii < verticesLength; ii += 2) {
for (int ii = 0; ii + 1 < verticesLength; ii += 2) {
float vx = vertices[ii], vy = vertices[ii + 1];
minX = Math.Min(minX, vx);
minY = Math.Min(minY, vy);

View File

@@ -595,7 +595,7 @@ namespace SpineRuntime40 {
}
if (vertices != null) {
for (int ii = 0; ii < verticesLength; ii += 2) {
for (int ii = 0; ii + 1 < verticesLength; ii += 2) {
float vx = vertices[ii], vy = vertices[ii + 1];
minX = Math.Min(minX, vx);
minY = Math.Min(minY, vy);

View File

@@ -641,7 +641,7 @@ namespace SpineRuntime41 {
}
if (vertices != null) {
for (int ii = 0; ii < verticesLength; ii += 2) {
for (int ii = 0; ii + 1 < verticesLength; ii += 2) {
float vx = vertices[ii], vy = vertices[ii + 1];
minX = Math.Min(minX, vx);
minY = Math.Min(minY, vy);

View File

@@ -742,7 +742,7 @@ namespace SpineRuntime42 {
verticesLength = clipper.ClippedVertices.Count;
}
for (int ii = 0; ii < verticesLength; ii += 2) {
for (int ii = 0; ii + 1 < verticesLength; ii += 2) {
float vx = vertices[ii], vy = vertices[ii + 1];
minX = Math.Min(minX, vx);
minY = Math.Min(minY, vy);

View File

@@ -1,10 +1,15 @@
using NLog;
using Microsoft.Win32;
using NLog;
using SpineViewer.Natives;
using SpineViewer.ViewModels.MainWindow;
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 +20,37 @@ namespace SpineViewer
/// </summary>
public partial class App : Application
{
public static string Version => Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
#if DEBUG
public const string AppName = "SpineViewer_D";
public const string ProgId = "SpineViewer_D.skel";
#else
public const string AppName = "SpineViewer";
public const string ProgId = "SpineViewer.skel";
#endif
public const string AutoRunFlag = "--autorun";
private const string MutexName = "__SpineViewerInstance__";
private const string PipeName = "__SpineViewerPipe__";
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 static readonly string AutoRunCommand = $"\"{ProcessPath}\" {AutoRunFlag}";
private static readonly string SkelFileDescription = $"SpineViewer File";
private static readonly string SkelIconFilePath = Path.Combine(ProcessDirectory, "Resources\\Images\\skel.ico");
private static readonly string ShellOpenCommand = $"\"{ProcessPath}\" \"%1\"";
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 +62,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 +88,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 +98,116 @@ namespace SpineViewer
LogManager.Configuration = config;
}
private static void ShowExistedInstance()
{
try
{
// 遍历同名进程
var processes = Process.GetProcessesByName(ProcessName);
foreach (var p in processes)
{
// 跳过当前进程
if (p.Id == Process.GetCurrentProcess().Id)
continue;
IntPtr hWnd = p.MainWindowHandle;
if (hWnd != IntPtr.Zero)
{
// 3. 显示并置顶窗口
if (User32.IsIconic(hWnd))
{
User32.ShowWindow(hWnd, User32.SW_RESTORE);
}
User32.SetForegroundWindow(hWnd);
break; // 找到一个就可以退出
}
}
}
catch
{
// 忽略异常,不影响当前进程退出
}
}
private static void SendCommandLineArgs()
{
var args = Environment.GetCommandLineArgs().Skip(1).ToArray();
if (args.Length <= 0)
return;
_logger.Info("Send command line args to existed instance, \"{0}\"", string.Join(", ", args));
try
{
// 已有实例在运行,把参数通过命名管道发过去
using (var client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out))
{
client.Connect(10000); // 10 秒超时
using (var writer = new StreamWriter(client))
{
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(() =>
{
var vm = (MainWindowViewModel)((MainWindow)Current.MainWindow).DataContext;
vm.SpineObjectListViewModel.AddSpineObjectFromFileList(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)
@@ -81,6 +217,116 @@ namespace SpineViewer
e.Handled = true;
}
public bool AutoRun
{
get
{
try
{
using (var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run"))
{
var command = key?.GetValue(AppName) as string;
return string.Equals(command, AutoRunCommand, StringComparison.OrdinalIgnoreCase);
}
}
catch (Exception ex)
{
_logger.Error("Failed to query autorun registry key, {0}", ex.Message);
_logger.Trace(ex.ToString());
return false;
}
}
set
{
try
{
if (value)
{
// 写入自启命令
using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run"))
{
key?.SetValue(AppName, AutoRunCommand);
}
}
else
{
// 删除自启命令
using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run"))
{
key?.DeleteValue(AppName, false);
}
}
}
catch (Exception ex)
{
_logger.Error("Failed to set autorun registry key, {0}", ex.Message);
_logger.Trace(ex.ToString());
}
}
}
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
{
if (value)
{
// 文件关联
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.NotifyAssociationChanged();
}
}
/// <summary>
/// 程序语言
/// </summary>
@@ -103,7 +349,6 @@ namespace SpineViewer
}
}
private AppLanguage _language = AppLanguage.ZH;
}
public enum AppLanguage
@@ -112,4 +357,4 @@ namespace SpineViewer
EN,
JA
}
}
}

View File

@@ -0,0 +1,48 @@
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 RootGridCol2Width { get; set; }
public double ModelListRow0Height { get; set; }
public double ModelListRow2Height { get; set; }
public double ExplorerGridRow0Height { get; set; }
public double ExplorerGridRow2Height { get; set; }
public double RightPanelGridRow0Height { get; set; }
public double RightPanelGridRow2Height { 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);
public Stretch BackgroundImageMode { get; set; } = Stretch.Uniform;
#endregion
}
}

View File

@@ -1,5 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Spine.SpineWrappers;
using SpineViewer.Services;
using System;
using System.Collections.Generic;
using System.IO;
@@ -73,12 +75,35 @@ namespace SpineViewer.Models
#region
[ObservableProperty]
private bool _renderSelectedOnly;
public RelayCommand Cmd_SelectAutoRunWorkspaceConfigPath => _cmd_SelectAutoRunWorkspaceConfigPath ??= new(() =>
{
if (!DialogService.ShowOpenJsonDialog(out var fileName))
return;
AutoRunWorkspaceConfigPath = fileName;
});
private RelayCommand? _cmd_SelectAutoRunWorkspaceConfigPath;
[ObservableProperty]
private AppLanguage _appLanguage;
[ObservableProperty]
private bool _renderSelectedOnly;
[ObservableProperty]
private bool _wallpaperView;
[ObservableProperty]
private bool? _closeToTray = null;
[ObservableProperty]
private bool _autoRun;
[ObservableProperty]
private string _autoRunWorkspaceConfigPath;
[ObservableProperty]
private bool _associateFileSuffix;
#endregion
}
}

View File

@@ -269,6 +269,12 @@ namespace SpineViewer.Models
entry = _spineObject.AnimationState.SetAnimation(index, name, true);
entry.TimeScale = lastTimeScale;
entry.Alpha = lastAlpha;
// XXX(#105): 部分 3.4.02 版本模型在设置动画后出现附件残留, 因此强制进行一次 Setup
if (_spineObject.Version == SpineVersion.V34)
{
_spineObject.Skeleton.SetSlotsToSetupPose();
}
changed = true;
}
}

View File

@@ -43,10 +43,9 @@ namespace SpineViewer.Models
public Color BackgroundColor { get; set; }
// TODO: 背景图片
//public string? BackgroundImagePath { get; set; }
public string BackgroundImagePath { get; set; }
//public ? BackgroundImageDisplayMode { get; set; }
public Stretch BackgroundImageMode { get; set; } = Stretch.Uniform;
}
public class SpineObjectWorkspaceConfigModel

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 IntPtr CreateCompatibleDC(IntPtr hdc);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern bool DeleteDC(IntPtr hdc);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern bool DeleteObject(IntPtr hObject);
}
}

View File

@@ -0,0 +1,28 @@
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
{
private const uint SHCNE_ASSOCCHANGED = 0x08000000;
private const uint SHCNF_IDLIST = 0x0000;
[DllImport("shell32.dll")]
private static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
public static void NotifyAssociationChanged()
{
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, IntPtr.Zero, IntPtr.Zero);
}
}
}

View File

@@ -0,0 +1,357 @@
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>
/// user32.dll 包装类
/// </summary>
public static class User32
{
public const int GWL_STYLE = -16;
public const int WS_OVERLAPPED = 0x00000000;
public const int WS_POPUP = unchecked((int)0x80000000);
public const int WS_CHILD = 0x40000000;
public const int WS_MINIMIZE = 0x20000000;
public const int WS_VISIBLE = 0x10000000;
public const int WS_DISABLED = 0x08000000;
public const int WS_CLIPSIBLINGS = 0x04000000;
public const int WS_CLIPCHILDREN = 0x02000000;
public const int WS_MAXIMIZE = 0x01000000;
public const int WS_BORDER = 0x00800000;
public const int WS_DLGFRAME = 0x00400000;
public const int WS_VSCROLL = 0x00200000;
public const int WS_HSCROLL = 0x00100000;
public const int WS_SYSMENU = 0x00080000;
public const int WS_THICKFRAME = 0x00040000;
public const int WS_GROUP = 0x00020000;
public const int WS_TABSTOP = 0x00010000;
public const int WS_MINIMIZEBOX = 0x00020000;
public const int WS_MAXIMIZEBOX = 0x00010000;
public const int WS_CHILDWINDOW = WS_CHILD;
public const int WS_CAPTION = WS_BORDER | WS_DLGFRAME;
public const int WS_TILED = WS_OVERLAPPED;
public const int WS_ICONIC = WS_MINIMIZE;
public const int WS_SIZEBOX = WS_THICKFRAME;
public const int WS_TILEDWINDOW = WS_OVERLAPPEDWINDOW;
public const int WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX;
public const int WS_POPUPWINDOW = WS_POPUP | WS_BORDER | WS_SYSMENU;
public const int GWL_EXSTYLE = -20;
public const int WS_EX_DLGMODALFRAME = 0x00000001;
public const int WS_EX_NOPARENTNOTIFY = 0x00000004;
public const int WS_EX_TOPMOST = 0x00000008;
public const int WS_EX_ACCEPTFILES = 0x00000010;
public const int WS_EX_TRANSPARENT = 0x00000020;
public const int WS_EX_MDICHILD = 0x00000040;
public const int WS_EX_TOOLWINDOW = 0x00000080;
public const int WS_EX_WINDOWEDGE = 0x00000100;
public const int WS_EX_CLIENTEDGE = 0x00000200;
public const int WS_EX_CONTEXTHELP = 0x00000400;
public const int WS_EX_RIGHT = 0x00001000;
public const int WS_EX_LEFT = 0x00000000;
public const int WS_EX_RTLREADING = 0x00002000;
public const int WS_EX_LTRREADING = 0x00000000;
public const int WS_EX_LEFTSCROLLBAR = 0x00004000;
public const int WS_EX_RIGHTSCROLLBAR = 0x00000000;
public const int WS_EX_CONTROLPARENT = 0x00010000;
public const int WS_EX_STATICEDGE = 0x00020000;
public const int WS_EX_APPWINDOW = 0x00040000;
public const int WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE;
public const int WS_EX_PALETTEWINDOW = WS_EX_WINDOWEDGE | WS_EX_TOOLWINDOW | WS_EX_TOPMOST;
public const int WS_EX_LAYERED = 0x00080000;
public const int WS_EX_NOINHERITLAYOUT = 0x00100000;
public const int WS_EX_LAYOUTRTL = 0x00400000;
public const int WS_EX_COMPOSITED = 0x02000000;
public const int WS_EX_NOACTIVATE = 0x08000000;
public const uint LWA_COLORKEY = 0x1;
public const uint LWA_ALPHA = 0x2;
public const byte AC_SRC_OVER = 0x00;
public const byte AC_SRC_ALPHA = 0x01;
public const int ULW_COLORKEY = 0x00000001;
public const int ULW_ALPHA = 0x00000002;
public const int ULW_OPAQUE = 0x00000004;
public const IntPtr HWND_TOPMOST = -1;
public const uint SWP_ASYNCWINDOWPOS = 0x4000;
public const uint SWP_DEFERERASE = 0x2000;
public const uint SWP_NOSENDCHANGING = 0x0400;
public const uint SWP_NOOWNERZORDER = 0x0200;
public const uint SWP_NOREPOSITION = 0x0200;
public const uint SWP_NOCOPYBITS = 0x0100;
public const uint SWP_HIDEWINDOW = 0x0080;
public const uint SWP_SHOWWINDOW = 0x0040;
public const uint SWP_DRAWFRAME = 0x0020;
public const uint SWP_FRAMECHANGED = 0x0020;
public const uint SWP_NOACTIVATE = 0x0010;
public const uint SWP_NOREDRAW = 0x0008;
public const uint SWP_NOZORDER = 0x0004;
public const uint SWP_NOMOVE = 0x0002;
public const uint SWP_NOSIZE = 0x0001;
public const int WM_SPAWN_WORKER = 0x052C; // 一个未公开的神秘消息
public const uint SMTO_NORMAL = 0x0000;
public const uint SMTO_BLOCK = 0x0001;
public const uint SMTO_ABORTIFHUNG = 0x0002;
public const uint SMTO_NOTIMEOUTIFNOTHUNG = 0x0008;
public const uint GA_PARENT = 1;
public const uint GW_OWNER = 4;
public const int SW_HIDE = 0;
public const int SW_SHOWNORMAL = 1;
public const int SW_SHOWMINIMIZED = 2;
public const int SW_SHOWMAXIMIZED = 3;
public const int SW_SHOWNOACTIVATE = 4;
public const int SW_SHOW = 5;
public const int SW_MINIMIZE = 6;
public const int SW_SHOWMINNOACTIVE = 7;
public const int SW_SHOWNA = 8;
public const int SW_RESTORE = 9;
public const int SW_SHOWDEFAULT = 10;
public const uint MONITOR_DEFAULTTONULL = 0;
public const uint MONITOR_DEFAULTTOPRIMARY = 1;
public const uint MONITOR_DEFAULTTONEAREST = 2;
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int x;
public int y;
}
[StructLayout(LayoutKind.Sequential)]
public struct SIZE
{
public int cx;
public int cy;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct BLENDFUNCTION
{
public byte BlendOp;
public byte BlendFlags;
public byte SourceConstantAlpha;
public byte AlphaFormat;
}
[StructLayout(LayoutKind.Sequential)]
private struct LASTINPUTINFO
{
public uint cbSize;
public uint dwTime;
}
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[StructLayout(LayoutKind.Sequential)]
private struct MONITORINFOEX
{
public uint cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string szDevice;
}
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool GetLayeredWindowAttributes(IntPtr hWnd, ref uint crKey, ref byte bAlpha, ref uint dwFlags);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool SetLayeredWindowAttributes(IntPtr hWnd, uint pcrKey, byte pbAlpha, uint pdwFlags);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool UpdateLayeredWindow(IntPtr hWnd, IntPtr hdcDst, IntPtr pptDst, ref SIZE psize, IntPtr hdcSrc, ref POINT pptSrc, int crKey, ref BLENDFUNCTION pblend, int dwFlags);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll", SetLastError = true)]
public static extern uint GetDoubleClickTime();
[DllImport("user32.dll", SetLastError = true)]
private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam, uint fuFlags, uint uTimeout, out IntPtr lpdwResult);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string className, string windowTitle);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetParent(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetAncestor(IntPtr hWnd, uint gaFlags);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
public static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam);
[DllImport("User32.dll")]
public static extern int SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
public static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);
[DllImport("user32.dll")]
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi);
[DllImport("user32.dll")]
public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
[DllImport("user32.dll")]
public static extern int GetWindowLong(IntPtr hWnd, int nIndex);
public static TimeSpan GetLastInputElapsedTime()
{
LASTINPUTINFO lastInputInfo = new();
lastInputInfo.cbSize = (uint)Marshal.SizeOf(lastInputInfo);
uint idleTimeMillis = 1000;
if (GetLastInputInfo(ref lastInputInfo))
{
uint tickCount = (uint)Environment.TickCount;
uint lastInputTick = lastInputInfo.dwTime;
idleTimeMillis = tickCount - lastInputTick;
}
return TimeSpan.FromMilliseconds(idleTimeMillis);
}
public static IntPtr GetWorkerW()
{
// NOTE: Codes borrowed from @rocksdanister/lively
var progman = FindWindow("Progman", null);
if (progman == IntPtr.Zero)
return IntPtr.Zero;
// Send 0x052C to Progman. This message directs Progman to spawn a
// WorkerW behind the desktop icons. If it is already there, nothing
// happens.
SendMessageTimeout(progman, WM_SPAWN_WORKER, 0, 0, SMTO_NORMAL, 1000, out _);
// Spy++ output
// .....
// 0x00010190 "" WorkerW
// ...
// 0x000100EE "" SHELLDLL_DefView
// 0x000100F0 "FolderView" SysListView32
// 0x00100B8A "" WorkerW <-- This is the WorkerW instance we are after!
// 0x000100EC "Program Manager" Progman
var workerw = IntPtr.Zero;
// We enumerate all Windows, until we find one, that has the SHELLDLL_DefView
// as a child.
// If we found that window, we take its next sibling and assign it to workerw.
EnumWindows(new EnumWindowsProc((tophandle, topparamhandle) =>
{
IntPtr p = FindWindowEx(tophandle, IntPtr.Zero, "SHELLDLL_DefView", null);
if (p != IntPtr.Zero)
{
// Gets the WorkerW Window after the current one.
workerw = FindWindowEx(IntPtr.Zero, tophandle, "WorkerW", null);
}
return true;
}), IntPtr.Zero);
// Some Windows 11 builds have a different Progman window layout.
// If the above code failed to find WorkerW, we should try this.
// Spy++ output
// 0x000100EC "Program Manager" Progman
// 0x000100EE "" SHELLDLL_DefView
// 0x000100F0 "FolderView" SysListView32
// 0x00100B8A "" WorkerW <-- This is the WorkerW instance we are after!
if (workerw == IntPtr.Zero)
{
workerw = FindWindowEx(progman, IntPtr.Zero, "WorkerW", null);
}
Debug.WriteLine($"HWND(WorkerW): {workerw:x8}");
return workerw;
}
public static bool GetScreenResolution(IntPtr hwnd, out uint width, out uint height)
{
IntPtr hMon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
var mi = new MONITORINFOEX { cbSize = (uint)Marshal.SizeOf<MONITORINFOEX>() };
if (GetMonitorInfo(hMon, ref mi))
{
int widthPx = mi.rcMonitor.Right - mi.rcMonitor.Left;
int heightPx = mi.rcMonitor.Bottom - mi.rcMonitor.Top;
width = (uint)widthPx;
height = (uint)heightPx;
return true;
}
width = height = 0;
return false;
}
public static bool GetPrimaryScreenResolution(out uint width, out uint height)
{
IntPtr hMon = MonitorFromWindow(IntPtr.Zero, MONITOR_DEFAULTTOPRIMARY);
var mi = new MONITORINFOEX { cbSize = (uint)Marshal.SizeOf<MONITORINFOEX>() };
if (GetMonitorInfo(hMon, ref mi))
{
int widthPx = mi.rcMonitor.Right - mi.rcMonitor.Left;
int heightPx = mi.rcMonitor.Bottom - mi.rcMonitor.Top;
width = (uint)widthPx;
height = (uint)heightPx;
return true;
}
width = height = 0;
return false;
}
}
}

View File

@@ -1,241 +0,0 @@
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>
/// Win32 Sdk 包装类
/// </summary>
public static class Win32
{
public const int GWL_STYLE = -16;
public const int WS_SIZEBOX = 0x40000;
public const int WS_BORDER = 0x800000;
public const int WS_VISIBLE = 0x10000000;
public const int WS_CHILD = 0x40000000;
public const int WS_POPUP = unchecked((int)0x80000000);
public const int GWL_EXSTYLE = -20;
public const int WS_EX_TOPMOST = 0x8;
public const int WS_EX_TRANSPARENT = 0x20;
public const int WS_EX_TOOLWINDOW = 0x80;
public const int WS_EX_WINDOWEDGE = 0x100;
public const int WS_EX_CLIENTEDGE = 0x200;
public const int WS_EX_APPWINDOW = 0x40000;
public const int WS_EX_LAYERED = 0x80000;
public const int WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE;
public const int WS_EX_NOACTIVATE = 0x8000000;
public const uint LWA_COLORKEY = 0x1;
public const uint LWA_ALPHA = 0x2;
public const byte AC_SRC_OVER = 0x00;
public const byte AC_SRC_ALPHA = 0x01;
public const int ULW_COLORKEY = 0x00000001;
public const int ULW_ALPHA = 0x00000002;
public const int ULW_OPAQUE = 0x00000004;
public const nint HWND_TOPMOST = -1;
public const uint SWP_NOSIZE = 0x0001;
public const uint SWP_NOMOVE = 0x0002;
public const uint SWP_NOZORDER = 0x0004;
public const uint SWP_FRAMECHANGED = 0x0020;
public const int WM_SPAWN_WORKER = 0x052C; // 一个未公开的神秘消息
public const uint SMTO_NORMAL = 0x0000;
public const uint SMTO_BLOCK = 0x0001;
public const uint SMTO_ABORTIFHUNG = 0x0002;
public const uint SMTO_NOTIMEOUTIFNOTHUNG = 0x0008;
public const uint GA_PARENT = 1;
public const uint GW_OWNER = 4;
public const int SW_HIDE = 0;
public const int SW_SHOWNORMAL = 1;
public const int SW_SHOWMINIMIZED = 2;
public const int SW_SHOWMAXIMIZED = 3;
public const int SW_SHOWNOACTIVATE = 4;
public const int SW_SHOW = 5;
public const int SW_MINIMIZE = 6;
public const int SW_SHOWMINNOACTIVE = 7;
public const int SW_SHOWNA = 8;
public const int SW_RESTORE = 9;
public const int SW_SHOWDEFAULT = 10;
public const uint MONITOR_DEFAULTTONEAREST = 2;
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int x;
public int y;
}
[StructLayout(LayoutKind.Sequential)]
public struct SIZE
{
public int cx;
public int cy;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct BLENDFUNCTION
{
public byte BlendOp;
public byte BlendFlags;
public byte SourceConstantAlpha;
public byte AlphaFormat;
}
[StructLayout(LayoutKind.Sequential)]
private struct LASTINPUTINFO
{
public uint cbSize;
public uint dwTime;
}
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[StructLayout(LayoutKind.Sequential)]
private struct MONITORINFOEX
{
public uint cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string szDevice;
}
[DllImport("user32.dll", SetLastError = true)]
public static extern nint GetDC(nint hWnd);
[DllImport("user32.dll", SetLastError = true)]
public static extern int ReleaseDC(nint hWnd, nint hDC);
[DllImport("user32.dll", SetLastError = true)]
public static extern int SetWindowLong(nint hWnd, int nIndex, int dwNewLong);
[DllImport("user32.dll", SetLastError = true)]
public static extern int GetWindowLong(nint hWnd, int nIndex);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool GetLayeredWindowAttributes(nint hWnd, ref uint crKey, ref byte bAlpha, ref uint dwFlags);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool SetLayeredWindowAttributes(nint hWnd, uint pcrKey, byte pbAlpha, uint pdwFlags);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool UpdateLayeredWindow(nint hWnd, nint hdcDst, nint pptDst, ref SIZE psize, nint hdcSrc, ref POINT pptSrc, int crKey, ref BLENDFUNCTION pblend, int dwFlags);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll", SetLastError = true)]
public static extern uint GetDoubleClickTime();
[DllImport("user32.dll", SetLastError = true)]
private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
[DllImport("user32.dll", SetLastError = true)]
public static extern nint FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll", SetLastError = true)]
public static extern nint SendMessageTimeout(nint hWnd, uint Msg, nint wParam, nint lParam, uint fuFlags, uint uTimeout, out nint lpdwResult);
[DllImport("user32.dll", SetLastError = true)]
public static extern nint FindWindowEx(nint parentHandle, nint childAfter, string className, string windowTitle);
[DllImport("user32.dll", SetLastError = true)]
public static extern nint SetParent(nint hWndChild, nint hWndNewParent);
[DllImport("user32.dll", SetLastError = true)]
public static extern nint GetParent(nint hWnd);
[DllImport("user32.dll", SetLastError = true)]
public static extern nint GetAncestor(nint hWnd, uint gaFlags);
[DllImport("user32.dll", SetLastError = true)]
public static extern nint GetWindow(nint hWnd, uint uCmd);
[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("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")]
private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);
[DllImport("user32.dll")]
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi);
public static TimeSpan GetLastInputElapsedTime()
{
LASTINPUTINFO lastInputInfo = new();
lastInputInfo.cbSize = (uint)Marshal.SizeOf(lastInputInfo);
uint idleTimeMillis = 1000;
if (GetLastInputInfo(ref lastInputInfo))
{
uint tickCount = (uint)Environment.TickCount;
uint lastInputTick = lastInputInfo.dwTime;
idleTimeMillis = tickCount - lastInputTick;
}
return TimeSpan.FromMilliseconds(idleTimeMillis);
}
public static nint GetWorkerW()
{
var progman = FindWindow("Progman", null);
if (progman == nint.Zero)
return nint.Zero;
nint hWnd = FindWindowEx(progman, 0, "WorkerW", null);
Debug.WriteLine($"HWND(Progman.WorkerW): {hWnd:x8}");
return hWnd;
}
public static bool GetScreenResolution(IntPtr hwnd, out uint width, out uint height)
{
IntPtr hMon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
var mi = new MONITORINFOEX { cbSize = (uint)Marshal.SizeOf<MONITORINFOEX>() };
if (GetMonitorInfo(hMon, ref mi))
{
int widthPx = mi.rcMonitor.Right - mi.rcMonitor.Left;
int heightPx = mi.rcMonitor.Bottom - mi.rcMonitor.Top;
width = (uint)widthPx;
height = (uint)heightPx;
return true;
}
width = height = 0;
return false;
}
}
}

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");
@@ -31,6 +33,7 @@ namespace SpineViewer.Resources
public static string Str_TooManyItemsToAddQuest => Get<string>("Str_TooManyItemsToAddQuest");
public static string Str_RemoveItemsQuest => Get<string>("Str_RemoveItemsQuest");
public static string Str_DeleteItemsQuest => Get<string>("Str_DeleteItemsQuest");
public static string Str_CloseToTrayQuest => Get<string>("Str_CloseToTrayQuest");
public static string Str_FrameExporterTitle => Get<string>("Str_FrameExporterTitle");
public static string Str_FrameSequenceExporterTitle => Get<string>("Str_FrameSequenceExporterTitle");

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>
@@ -115,10 +118,12 @@
<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_WallpaperView">Wallpaper View</s:String>
<s:String x:Key="Str_RenderSelectedOnly">Render Selected Only</s:String>
<s:String x:Key="Str_ShowAxis">Show Axis</s:String>
<s:String x:Key="Str_BackgroundColor">Background Color</s:String>
<s:String x:Key="Str_BackgroundImage">Background Image</s:String>
<s:String x:Key="Str_BackgroundImagePath">Background Image Path</s:String>
<s:String x:Key="Str_BackgroundImageMode">Background Image Mode</s:String>
<!-- 渲染画面按钮组 -->
<s:String x:Key="Str_StopTooltip">Stop</s:String>
@@ -144,6 +149,7 @@
<s:String x:Key="Str_TooManyItemsToAddQuest">{0} items total, add all at once?</s:String>
<s:String x:Key="Str_RemoveItemsQuest">Remove {0} items?</s:String>
<s:String x:Key="Str_DeleteItemsQuest">Delete {0} items?</s:String>
<s:String x:Key="Str_CloseToTrayQuest" xml:space="preserve">You clicked the close button. Do you want to minimize to the tray icon instead of closing the application directly?&#x0A;(If you choose Yes, the window will be minimized to the tray and can be restored by double-clicking the icon. You can change this option later in the application preferences.)</s:String>
<!-- 导出对话框弹窗文本 -->
<s:String x:Key="Str_FrameExporterTitle">Export Single Frame</s:String>
@@ -229,7 +235,14 @@
<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_Language">Language</s:String>
<s:String x:Key="Str_CloseToTray">Minimize to tray when closing</s:String>
<s:String x:Key="Str_AutoRun">Auto Start</s:String>
<s:String x:Key="Str_AutoRunWorkspaceConfigPath">Auto-load Workspace File on Startup</s:String>
<s:String x:Key="Str_AutoRunWorkspaceConfigPathTooltip">Specifies the workspace configuration file to be automatically loaded when the program starts with Windows startup. This takes effect only if auto-startup is enabled.</s:String>
<s:String x:Key="Str_AssociateFileSuffix">Associate File Extension</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>
@@ -115,10 +118,12 @@
<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_WallpaperView">壁紙表示</s:String>
<s:String x:Key="Str_RenderSelectedOnly">選択のみレンダリング</s:String>
<s:String x:Key="Str_ShowAxis">座標軸を表示</s:String>
<s:String x:Key="Str_BackgroundColor">背景色</s:String>
<s:String x:Key="Str_BackgroundImage">背景画像</s:String>
<s:String x:Key="Str_BackgroundImagePath">背景画像のパス</s:String>
<s:String x:Key="Str_BackgroundImageMode">背景画像のモード</s:String>
<!-- 渲染画面按钮组 -->
<s:String x:Key="Str_StopTooltip">停止</s:String>
@@ -144,6 +149,7 @@
<s:String x:Key="Str_TooManyItemsToAddQuest">全{0}件、一度に追加しますか?</s:String>
<s:String x:Key="Str_RemoveItemsQuest">{0}件を削除してもよろしいですか?</s:String>
<s:String x:Key="Str_DeleteItemsQuest">{0}件を削除してもよろしいですか?</s:String>
<s:String x:Key="Str_CloseToTrayQuest" xml:space="preserve">閉じるボタンをクリックしました。アプリケーションを直接終了するのではなく、通知領域のアイコンに最小化しますか?&#x0A;(「はい」を選択すると、ウィンドウは通知領域に最小化され、アイコンをダブルクリックすると復元できます。この設定は後でアプリケーションの環境設定から変更できます。)</s:String>
<!-- 导出对话框弹窗文本 -->
<s:String x:Key="Str_FrameExporterTitle">単一フレームをエクスポート</s:String>
@@ -229,8 +235,15 @@
<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_Language">言語</s:String>
<s:String x:Key="Str_CloseToTray">閉じるときにトレイに最小化する</s:String>
<s:String x:Key="Str_AutoRun">自動起動</s:String>
<s:String x:Key="Str_AutoRunWorkspaceConfigPath">起動時にワークスペースファイルを自動読み込み</s:String>
<s:String x:Key="Str_AutoRunWorkspaceConfigPathTooltip">プログラムが Windows 起動と同時に自動起動した場合に、自動的に読み込むワークスペース設定ファイルを指定します。自動起動が有効な場合にのみ適用されます。</s:String>
<s:String x:Key="Str_AssociateFileSuffix">ファイル拡張子を関連付ける</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>
@@ -115,10 +118,12 @@
<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_WallpaperView">桌面投影</s:String>
<s:String x:Key="Str_RenderSelectedOnly">仅渲染选中</s:String>
<s:String x:Key="Str_ShowAxis">显示坐标轴</s:String>
<s:String x:Key="Str_BackgroundColor">背景颜色</s:String>
<s:String x:Key="Str_BackgroundImage">背景图片</s:String>
<s:String x:Key="Str_BackgroundImagePath">背景图片路径</s:String>
<s:String x:Key="Str_BackgroundImageMode">背景图片模式</s:String>
<!-- 渲染画面按钮组 -->
<s:String x:Key="Str_StopTooltip">停止</s:String>
@@ -144,6 +149,7 @@
<s:String x:Key="Str_TooManyItemsToAddQuest">共 {0} 项,是否一次性添加?</s:String>
<s:String x:Key="Str_RemoveItemsQuest">确定移除 {0} 项?</s:String>
<s:String x:Key="Str_DeleteItemsQuest">确定删除 {0} 项?</s:String>
<s:String x:Key="Str_CloseToTrayQuest" xml:space="preserve">您点击了关闭按钮,是否需要最小化至托盘图标而不是直接关闭?&#x0A;(选是则最小化至托盘图标,可以通过双击图标还原窗口,以后也可以在程序首选项中重新设置该选项)</s:String>
<!-- 导出对话框弹窗文本 -->
<s:String x:Key="Str_FrameExporterTitle">导出单帧</s:String>
@@ -229,7 +235,14 @@
<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_Language">语言</s:String>
<s:String x:Key="Str_CloseToTray">关闭时最小化至托盘图标</s:String>
<s:String x:Key="Str_AutoRun">开机自启</s:String>
<s:String x:Key="Str_AutoRunWorkspaceConfigPath">自启动加载工作区文件</s:String>
<s:String x:Key="Str_AutoRunWorkspaceConfigPathTooltip">设置程序开机自启后自动加载的工作区配置文件,仅在启用开机自启时生效</s:String>
<s:String x:Key="Str_AssociateFileSuffix">关联文件后缀</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>
@@ -78,6 +90,22 @@ namespace SpineViewer.Services
return false;
}
public static bool ShowOpenSFMLImageDialog(out string? fileName, string initialDirectory = "")
{
var dialog = new OpenFileDialog()
{
InitialDirectory = initialDirectory,
Filter = "SFML Image|*.png;*.jpg;*.jpeg;*.bmp;*.tga|All|*.*"
};
if (dialog.ShowDialog() is true)
{
fileName = dialog.FileName;
return true;
}
fileName = null;
return false;
}
public static bool ShowOpenJsonDialog(out string? fileName, string initialDirectory = "")
{
var dialog = new OpenFileDialog()

View File

@@ -28,10 +28,16 @@ namespace SpineViewer.Services
MessageBox.Show(text, title, MessageBoxButton.OK, MessageBoxImage.Error);
}
public static bool Quest(string text, string? title = null)
public static bool OKCancel(string text, string? title = null)
{
title ??= AppResource.Str_QuestPopup;
return MessageBox.Show(text, title, MessageBoxButton.OKCancel, MessageBoxImage.Question) == MessageBoxResult.OK;
}
public static bool YesNo(string text, string? title = null)
{
title ??= AppResource.Str_QuestPopup;
return MessageBox.Show(text, title, MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes;
}
}
}

View File

@@ -7,19 +7,24 @@
<TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.15.12</Version>
<Version>0.16.0</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">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>

View File

@@ -163,6 +163,7 @@ namespace SpineViewer.ViewModels.MainWindow
Size = new(bounds.Width, -bounds.Height),
Format = SkiaSharp.SKEncodedImageFormat.Webp,
Quality = PreviewQuality,
BackgroundColor = SFML.Graphics.Color.Transparent,
};
exporter.Export(m.PreviewFilePath, sp);
}
@@ -199,6 +200,7 @@ namespace SpineViewer.ViewModels.MainWindow
{
Format = SkiaSharp.SKEncodedImageFormat.Webp,
Quality = PreviewQuality,
BackgroundColor = SFML.Graphics.Color.Transparent,
};
for (int i = 0; i < totalCount; i++)
{
@@ -247,7 +249,7 @@ namespace SpineViewer.ViewModels.MainWindow
private void DeletePreview_Execute(IList? args)
{
if (args is null || args.Count <= 0) return;
if (!MessagePopupService.Quest(string.Format(AppResource.Str_DeleteItemsQuest, args.Count))) return;
if (!MessagePopupService.OKCancel(string.Format(AppResource.Str_DeleteItemsQuest, args.Count))) return;
if (args.Count <= 10)
{

View File

@@ -5,6 +5,7 @@ using SFMLRenderer;
using SpineViewer.Models;
using SpineViewer.Services;
using SpineViewer.Utils;
using System.Windows;
using System.Windows.Shell;
namespace SpineViewer.ViewModels.MainWindow
@@ -16,9 +17,10 @@ namespace SpineViewer.ViewModels.MainWindow
{
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
public MainWindowViewModel(ISFMLRenderer sfmlRenderer)
public MainWindowViewModel(ISFMLRenderer sfmlRenderer, ISFMLRenderer wallpaperRenderer)
{
_sfmlRenderer = sfmlRenderer;
_wallpaperRenderer = wallpaperRenderer;
_explorerListViewModel = new(this);
_spineObjectListViewModel = new(this);
_sfmlRendererViewModel = new(this);
@@ -27,12 +29,35 @@ namespace SpineViewer.ViewModels.MainWindow
public string Title => $"SpineViewer - v{App.Version}";
/// <summary>
/// 指示是否通过托盘图标进行退出
/// </summary>
public bool IsShuttingDownFromTray => _isShuttingDownFromTray;
private bool _isShuttingDownFromTray;
public bool? CloseToTray
{
get => _closeToTray;
set => SetProperty(ref _closeToTray, value);
}
private bool? _closeToTray = null;
public string AutoRunWorkspaceConfigPath
{
get => _autoRunWorkspaceConfigPath;
set => SetProperty(ref _autoRunWorkspaceConfigPath, value);
}
private string _autoRunWorkspaceConfigPath;
/// <summary>
/// SFML 渲染对象
/// </summary>
public ISFMLRenderer SFMLRenderer => _sfmlRenderer;
private readonly ISFMLRenderer _sfmlRenderer;
public ISFMLRenderer WallpaperRenderer => _wallpaperRenderer;
private readonly ISFMLRenderer _wallpaperRenderer;
public TaskbarItemProgressState ProgressState { get => _progressState; set => SetProperty(ref _progressState, value); }
private TaskbarItemProgressState _progressState = TaskbarItemProgressState.None;
@@ -45,6 +70,9 @@ namespace SpineViewer.ViewModels.MainWindow
public ObservableCollectionWithLock<SpineObjectModel> SpineObjects => _spineObjectModels;
private readonly ObservableCollectionWithLock<SpineObjectModel> _spineObjectModels = [];
/// <summary>
/// 首选项 ViewModel
/// </summary>
public PreferenceViewModel PreferenceViewModel => _preferenceViewModel;
private readonly PreferenceViewModel _preferenceViewModel;
@@ -72,6 +100,21 @@ namespace SpineViewer.ViewModels.MainWindow
public SFMLRendererViewModel SFMLRendererViewModel => _sfmlRendererViewModel;
private readonly SFMLRendererViewModel _sfmlRendererViewModel;
public RelayCommand Cmd_SwitchWallpaperView => _cmd_SwitchWallpaperView ??= new(() =>
{
_preferenceViewModel.WallpaperView = !_preferenceViewModel.WallpaperView;
_preferenceViewModel.SavePreference();
});
private RelayCommand _cmd_SwitchWallpaperView;
public RelayCommand Cmd_ExitFromTray => _cmd_ExitFromTray ??= new(() =>
{
_isShuttingDownFromTray = true;
OnPropertyChanged(nameof(IsShuttingDownFromTray));
App.Current.Shutdown();
});
private RelayCommand? _cmd_ExitFromTray;
/// <summary>
/// 打开工作区
/// </summary>
@@ -131,18 +174,5 @@ namespace SpineViewer.ViewModels.MainWindow
}
}
/// <summary>
/// 调试命令
/// </summary>
public RelayCommand Cmd_Debug => _cmd_Debug ??= new(Debug_Execute);
private RelayCommand? _cmd_Debug;
private void Debug_Execute()
{
#if DEBUG
MessagePopupService.Quest("测试一下");
#endif
}
}
}

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;
@@ -63,8 +66,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>
@@ -93,8 +107,13 @@ namespace SpineViewer.ViewModels.MainWindow
DebugPoints = DebugPoints,
DebugClippings = DebugClippings,
RenderSelectedOnly = RenderSelectedOnly,
AppLanguage = AppLanguage,
RenderSelectedOnly = RenderSelectedOnly,
WallpaperView = WallpaperView,
CloseToTray = CloseToTray,
AutoRun = AutoRun,
AutoRunWorkspaceConfigPath = AutoRunWorkspaceConfigPath,
AssociateFileSuffix = AssociateFileSuffix,
};
}
set
@@ -117,8 +136,13 @@ namespace SpineViewer.ViewModels.MainWindow
DebugPoints = value.DebugPoints;
DebugClippings = value.DebugClippings;
RenderSelectedOnly = value.RenderSelectedOnly;
AppLanguage = value.AppLanguage;
RenderSelectedOnly = value.RenderSelectedOnly;
WallpaperView = value.WallpaperView;
CloseToTray = value.CloseToTray;
AutoRun = value.AutoRun;
AutoRunWorkspaceConfigPath = value.AutoRunWorkspaceConfigPath;
AssociateFileSuffix = value.AssociateFileSuffix;
}
}
@@ -224,18 +248,48 @@ namespace SpineViewer.ViewModels.MainWindow
public static ImmutableArray<AppLanguage> AppLanguageOptions { get; } = Enum.GetValues<AppLanguage>().ToImmutableArray();
public bool RenderSelectedOnly
{
get => _vmMain.SFMLRendererViewModel.RenderSelectedOnly;
set => SetProperty(_vmMain.SFMLRendererViewModel.RenderSelectedOnly, value, v => _vmMain.SFMLRendererViewModel.RenderSelectedOnly = v);
}
public AppLanguage AppLanguage
{
get => ((App)App.Current).Language;
set => SetProperty(((App)App.Current).Language, value, v => ((App)App.Current).Language = v);
}
public bool RenderSelectedOnly
{
get => _vmMain.SFMLRendererViewModel.RenderSelectedOnly;
set => SetProperty(_vmMain.SFMLRendererViewModel.RenderSelectedOnly, value, v => _vmMain.SFMLRendererViewModel.RenderSelectedOnly = v);
}
public bool WallpaperView
{
get => _vmMain.SFMLRendererViewModel.WallpaperView;
set => SetProperty(_vmMain.SFMLRendererViewModel.WallpaperView, value, v => _vmMain.SFMLRendererViewModel.WallpaperView = v);
}
public bool? CloseToTray
{
get => _vmMain.CloseToTray;
set => SetProperty(_vmMain.CloseToTray, value, v => _vmMain.CloseToTray = v);
}
public bool AutoRun
{
get => ((App)App.Current).AutoRun;
set => SetProperty(((App)App.Current).AutoRun, value, v => ((App)App.Current).AutoRun = v);
}
public string AutoRunWorkspaceConfigPath
{
get => _vmMain.AutoRunWorkspaceConfigPath;
set => SetProperty(_vmMain.AutoRunWorkspaceConfigPath, value, v => _vmMain.AutoRunWorkspaceConfigPath = v);
}
public bool AssociateFileSuffix
{
get => ((App)App.Current).AssociateFileSuffix;
set => SetProperty(((App)App.Current).AssociateFileSuffix, value, v => ((App)App.Current).AssociateFileSuffix = v);
}
#endregion
}
}

View File

@@ -10,8 +10,10 @@ using SpineViewer.Services;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -23,6 +25,8 @@ namespace SpineViewer.ViewModels.MainWindow
{
public class SFMLRendererViewModel : ObservableObject
{
public ImmutableArray<Stretch> StretchOptions { get; } = Enum.GetValues<Stretch>().ToImmutableArray();
/// <summary>
/// 日志器
/// </summary>
@@ -31,6 +35,7 @@ namespace SpineViewer.ViewModels.MainWindow
private readonly MainWindowViewModel _vmMain;
private readonly ObservableCollectionWithLock<SpineObjectModel> _models;
private readonly ISFMLRenderer _renderer;
private readonly ISFMLRenderer _wallpaperRenderer;
/// <summary>
/// 被选中对象的背景颜色
@@ -69,6 +74,13 @@ namespace SpineViewer.ViewModels.MainWindow
private float _forwardDelta = 0;
private readonly object _forwardDeltaLock = new();
/// <summary>
/// 背景图片
/// </summary>
private SFML.Graphics.Sprite? _backgroundImageSprite; // XXX: 暂时未使用 Dispose 释放
private SFML.Graphics.Texture? _backgroundImageTexture; // XXX: 暂时未使用 Dispose 释放
private readonly object _bgLock = new();
/// <summary>
/// 临时变量, 记录拖放世界源点
/// </summary>
@@ -79,6 +91,7 @@ namespace SpineViewer.ViewModels.MainWindow
_vmMain = vmMain;
_models = _vmMain.SpineObjects;
_renderer = _vmMain.SFMLRenderer;
_wallpaperRenderer = _vmMain.WallpaperRenderer;
}
/// <summary>
@@ -86,6 +99,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;
@@ -161,6 +182,71 @@ namespace SpineViewer.ViewModels.MainWindow
}
private SFML.Graphics.Color _backgroundColor = new(105, 105, 105);
public string BackgroundImagePath
{
get => _backgroundImagePath;
set => SetProperty(_backgroundImagePath, value, v =>
{
if (string.IsNullOrWhiteSpace(v))
{
lock (_bgLock)
{
_backgroundImageSprite?.Dispose();
_backgroundImageTexture?.Dispose();
_backgroundImageTexture = null;
_backgroundImageSprite = null;
}
_backgroundImagePath = v;
}
else
{
if (!File.Exists(v))
{
_logger.Warn("Omit non-existed background image path, {0}", v);
return;
}
SFML.Graphics.Texture tex = null;
SFML.Graphics.Sprite sprite = null;
try
{
tex = new(v);
sprite = new(tex) { Origin = new(tex.Size.X / 2f, tex.Size.Y / 2f) };
lock (_bgLock)
{
_backgroundImageSprite?.Dispose();
_backgroundImageTexture?.Dispose();
_backgroundImageTexture = tex;
_backgroundImageSprite = sprite;
}
_backgroundImagePath = v;
_logger.Info("Load background image from {0}", v);
_logger.LogCurrentProcessMemoryUsage();
}
catch (Exception ex)
{
sprite?.Dispose();
tex?.Dispose();
_logger.Error("Failed to load background image from path: {0}, {1}", v, ex.Message);
}
}
});
}
private string _backgroundImagePath;
public Stretch BackgroundImageMode
{
get => _backgroundImageMode;
set => SetProperty(ref _backgroundImageMode, value);
}
private Stretch _backgroundImageMode = Stretch.Uniform;
public bool WallpaperView
{
get => _wallpaperView;
set => SetProperty(ref _wallpaperView, value);
}
private bool _wallpaperView;
public bool RenderSelectedOnly
{
get => _renderSelectedOnly;
@@ -181,6 +267,14 @@ namespace SpineViewer.ViewModels.MainWindow
}
private bool _isUpdating = true;
public RelayCommand Cmd_SelectBackgroundImage => _cmd_SelectBackgroundImage ??= new(() =>
{
if (!DialogService.ShowOpenSFMLImageDialog(out var fileName))
return;
BackgroundImagePath = fileName;
});
private RelayCommand? _cmd_SelectBackgroundImage;
public RelayCommand Cmd_Stop => _cmd_Stop ??= new(() =>
{
IsUpdating = false;
@@ -358,6 +452,7 @@ namespace SpineViewer.ViewModels.MainWindow
{
try
{
_wallpaperRenderer.SetActive(true);
_renderer.SetActive(true);
float delta;
@@ -376,8 +471,52 @@ namespace SpineViewer.ViewModels.MainWindow
_forwardDelta = 0;
}
using var v = _renderer.GetView();
_renderer.Clear(_backgroundColor);
if (_wallpaperView)
{
_wallpaperRenderer.SetView(v);
_wallpaperRenderer.Clear(_backgroundColor);
}
// 渲染背景
lock (_bgLock)
{
if (_backgroundImageSprite is not null)
{
using var view = _renderer.GetView();
var bg = _backgroundImageSprite;
var viewSize = view.Size;
var bgSize = bg.Texture.Size;
var scaleX = Math.Abs(viewSize.X / bgSize.X);
var scaleY = Math.Abs(viewSize.Y / bgSize.Y);
var signX = Math.Sign(viewSize.X);
var signY = Math.Sign(viewSize.Y);
if (_backgroundImageMode == Stretch.None)
{
scaleX = scaleY = 1f / _renderer.Zoom;
}
else if (_backgroundImageMode == Stretch.Uniform)
{
scaleX = scaleY = Math.Min(scaleX, scaleY);
}
else if (_backgroundImageMode == Stretch.UniformToFill)
{
scaleX = scaleY = Math.Max(scaleX, scaleY);
}
bg.Scale = new(signX * scaleX, signY * scaleY);
bg.Position = view.Center;
bg.Rotation = view.Rotation;
_renderer.Draw(bg);
if (_wallpaperView)
{
_wallpaperRenderer.Draw(bg);
}
}
}
if (_showAxis)
{
// 画一个很长的坐标轴, 用 1e9 比较合适
@@ -414,10 +553,20 @@ namespace SpineViewer.ViewModels.MainWindow
sp.EnableDebug = true;
_renderer.Draw(sp);
sp.EnableDebug = false;
if (_wallpaperView)
{
_wallpaperRenderer.Draw(sp);
}
}
}
_renderer.Display();
if (_wallpaperView)
{
_wallpaperRenderer.Display();
}
}
}
catch (Exception ex)
@@ -429,12 +578,12 @@ namespace SpineViewer.ViewModels.MainWindow
finally
{
_renderer.SetActive(false);
_wallpaperRenderer.SetActive(false);
}
}
public RendererWorkspaceConfigModel WorkspaceConfig
{
// TODO: 背景图片
get
{
return new()
@@ -451,12 +600,13 @@ namespace SpineViewer.ViewModels.MainWindow
Speed = Speed,
ShowAxis = ShowAxis,
BackgroundColor = BackgroundColor,
BackgroundImagePath = BackgroundImagePath,
BackgroundImageMode = BackgroundImageMode,
};
}
set
{
ResolutionX = value.ResolutionX;
ResolutionY = value.ResolutionY;
SetResolution(value.ResolutionX, value.ResolutionY);
CenterX = value.CenterX;
CenterY = value.CenterY;
Zoom = value.Zoom;
@@ -467,6 +617,8 @@ namespace SpineViewer.ViewModels.MainWindow
Speed = value.Speed;
ShowAxis = value.ShowAxis;
BackgroundColor = value.BackgroundColor;
BackgroundImagePath = value.BackgroundImagePath;
BackgroundImageMode = value.BackgroundImageMode;
}
}
}

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>
@@ -116,7 +127,7 @@ namespace SpineViewer.ViewModels.MainWindow
if (args.Count > 1)
{
if (!MessagePopupService.Quest(string.Format(AppResource.Str_RemoveItemsQuest, args.Count)))
if (!MessagePopupService.OKCancel(string.Format(AppResource.Str_RemoveItemsQuest, args.Count)))
return;
}
@@ -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.OKCancel(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>
@@ -174,6 +213,8 @@ namespace SpineViewer.ViewModels.MainWindow
spNew.ObjectConfig = sp.ObjectConfig;
_spineObjectModels[idx] = spNew;
sp.Dispose();
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, spNew));
}
catch (Exception ex)
{
@@ -229,6 +270,11 @@ namespace SpineViewer.ViewModels.MainWindow
_spineObjectModels[idx] = spNew;
sp.Dispose();
success++;
Application.Current.Dispatcher.BeginInvoke(() =>
{
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, spNew));
});
}
catch (Exception ex)
{
@@ -423,7 +469,7 @@ namespace SpineViewer.ViewModels.MainWindow
{
if (validPaths.Count > 100)
{
if (!MessagePopupService.Quest(string.Format(AppResource.Str_TooManyItemsToAddQuest, validPaths.Count)))
if (!MessagePopupService.OKCancel(string.Format(AppResource.Str_TooManyItemsToAddQuest, validPaths.Count)))
return;
}
ProgressService.RunAsync((pr, ct) => AddSpineObjectsTask(
@@ -457,7 +503,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 +526,7 @@ namespace SpineViewer.ViewModels.MainWindow
}
/// <summary>
/// 安全地在末尾添加一个模型, 发生错误会输出日志
/// 安全地在列表头添加一个模型, 发生错误会输出日志
/// </summary>
/// <returns>是否添加成功</returns>
private bool AddSpineObject(string skelPath, string? atlasPath = null)
@@ -488,7 +534,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 +558,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 +620,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 +648,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

@@ -334,27 +334,31 @@ namespace SpineViewer.ViewModels.MainWindow
public ObservableCollection<SkinViewModel> Skins => _skins;
public RelayCommand<IList?> Cmd_EnableSkins { get; } = new(
public RelayCommand<IList?> Cmd_EnableSkins => _cmd_EnableSkins ??= new (
args => { if (args is null) return; foreach (var s in args.OfType<SkinViewModel>()) s.Status = true; },
args => { return args is not null && args.OfType<SkinViewModel>().Any(); }
);
private RelayCommand<IList?> _cmd_EnableSkins;
public RelayCommand<IList?> Cmd_DisableSkins { get; } = new(
public RelayCommand<IList?> Cmd_DisableSkins => _cmd_DisableSkins ??= new (
args => { if (args is null) return; foreach (var s in args.OfType<SkinViewModel>()) s.Status = false; },
args => { return args is not null && args.OfType<SkinViewModel>().Any(); }
);
private RelayCommand<IList?> _cmd_DisableSkins;
public ObservableCollection<SlotViewModel> Slots => _slots;
public RelayCommand<IList?> Cmd_EnableSlots { get; } = new(
public RelayCommand<IList?> Cmd_EnableSlots => _cmd_EnableSlots ??= new (
args => { if (args is null) return; foreach (var s in args.OfType<SlotViewModel>()) s.Visible = true; },
args => { return args is not null && args.OfType<SlotViewModel>().Any(); }
);
private RelayCommand<IList?> _cmd_EnableSlots;
public RelayCommand<IList?> Cmd_DisableSlots { get; } = new(
public RelayCommand<IList?> Cmd_DisableSlots => _cmd_DisableSlots ??= new (
args => { if (args is null) return; foreach (var s in args.OfType<SlotViewModel>()) s.Visible = false; },
args => { return args is not null && args.OfType<SlotViewModel>().Any(); }
);
private RelayCommand<IList?> _cmd_DisableSlots;
public ObservableCollection<AnimationTrackViewModel> AnimationTracks => _animationTracks;

View File

@@ -64,7 +64,7 @@ namespace SpineViewer.ViewModels
private void Cancel_Execute()
{
if (!Cancel_CanExecute()) return;
if (!MessagePopupService.Quest(AppResource.Str_CancelQuest)) return;
if (!MessagePopupService.OKCancel(AppResource.Str_CancelQuest)) return;
_cts.Cancel();
Cmd_Cancel.NotifyCanExecuteChanged();
}

View File

@@ -21,73 +21,120 @@
<Button Width="120" Content="{DynamicResource Str_CopyDiagnosticsInfo}" Command="{Binding Cmd_CopyToClipboard}"/>
</Border>
<Border>
<Border Grid.IsSharedSizeScope="True">
<Border.Resources>
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource LabelDefault}">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
</Style>
<Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource TextBoxBaseStyle}">
<Setter Property="HorizontalContentAlignment" Value="Left"/>
</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="Left"/>
</Style>
<Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource TextBoxBaseStyle}">
<Setter Property="HorizontalContentAlignment" Value="Left"/>
</Style>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<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>
<StackPanel Margin="30 10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="CPU"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding CPU, Mode=OneWay}"/>
</Grid>
<Label Grid.Row="0" Grid.Column="0" Content="CPU"/>
<TextBox Grid.Row="0" Grid.Column="1" IsReadOnly="True" Text="{Binding CPU, Mode=OneWay}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="GPU"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding GPU, Mode=OneWay}"/>
</Grid>
<Label Grid.Row="1" Grid.Column="0" Content="GPU"/>
<TextBox Grid.Row="1" Grid.Column="1" IsReadOnly="True" Text="{Binding GPU, Mode=OneWay}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="Memory"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding Memory, Mode=OneWay}"/>
</Grid>
<Label Grid.Row="2" Grid.Column="0" Content="Memory"/>
<TextBox Grid.Row="2" Grid.Column="1" IsReadOnly="True" Text="{Binding Memory, Mode=OneWay}"/>
<Separator Height="10"/>
<Separator Grid.Row="3" Grid.ColumnSpan="2" Height="10"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="WindowsVersion"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding WindowsVersion, Mode=OneWay}"/>
</Grid>
<Label Grid.Row="4" Grid.Column="0" Content="WindowsVersion"/>
<TextBox Grid.Row="4" Grid.Column="1" IsReadOnly="True" Text="{Binding WindowsVersion, Mode=OneWay}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="DotNetVersion"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding DotNetVersion, Mode=OneWay}"/>
</Grid>
<Label Grid.Row="5" Grid.Column="0" Content="DotNetVersion"/>
<TextBox Grid.Row="5" Grid.Column="1" IsReadOnly="True" Text="{Binding DotNetVersion, Mode=OneWay}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="ProgramVersion"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ProgramVersion, Mode=OneWay}"/>
</Grid>
<Label Grid.Row="6" Grid.Column="0" Content="ProgramVersion"/>
<TextBox Grid.Row="6" Grid.Column="1" IsReadOnly="True" Text="{Binding ProgramVersion, Mode=OneWay}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="NLogVersion"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding NLogVersion, Mode=OneWay}"/>
</Grid>
<Label Grid.Row="7" Grid.Column="0" Content="NLogVersion"/>
<TextBox Grid.Row="7" Grid.Column="1" IsReadOnly="True" Text="{Binding NLogVersion, Mode=OneWay}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="SFMLVersion"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding SFMLVersion, Mode=OneWay}"/>
</Grid>
<Label Grid.Row="8" Grid.Column="0" Content="SFMLVersion"/>
<TextBox Grid.Row="8" Grid.Column="1" IsReadOnly="True" Text="{Binding SFMLVersion, Mode=OneWay}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="FFMpegCoreVersion"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding FFMpegCoreVersion, Mode=OneWay}"/>
</Grid>
<Label Grid.Row="9" Grid.Column="0" Content="FFMpegCoreVersion"/>
<TextBox Grid.Row="9" Grid.Column="1" IsReadOnly="True" Text="{Binding FFMpegCoreVersion, Mode=OneWay}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="SkiaSharpVersion"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding SkiaSharpVersion, Mode=OneWay}"/>
</Grid>
<Label Grid.Row="10" Grid.Column="0" Content="SkiaSharpVersion"/>
<TextBox Grid.Row="10" Grid.Column="1" IsReadOnly="True" Text="{Binding SkiaSharpVersion, Mode=OneWay}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="HandyControlVersion"/>
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding HandyControlVersion, Mode=OneWay}"/>
</Grid>
</StackPanel>
<Label Grid.Row="11" Grid.Column="0" Content="HandyControlVersion"/>
<TextBox Grid.Row="11" Grid.Column="1" IsReadOnly="True" Text="{Binding HandyControlVersion, Mode=OneWay}"/>
</Grid>
</ScrollViewer>
</Border>
</DockPanel>

View File

@@ -9,6 +9,7 @@
xmlns:utils="clr-namespace:SpineViewer.Utils"
xmlns:SFMLRenderer="clr-namespace:SFMLRenderer;assembly=SFMLRenderer"
mc:Ignorable="d"
x:Name="_mainWindow"
Title="{Binding Title}"
Width="1500"
Height="800"
@@ -68,7 +69,7 @@
<MenuItem Header="{DynamicResource Str_Diagnostics}" Command="{Binding Cmd_ShowDiagnosticsDialog}"/>
<Separator/>
<MenuItem Header="{DynamicResource Str_Abount}" Command="{Binding Cmd_ShowAboutDialog}"/>
<MenuItem Header="{DynamicResource Str_Debug}" Command="{Binding Cmd_Debug}"/>
<MenuItem Header="{DynamicResource Str_Debug}" Click="DebugMenuItem_Click"/>
</MenuItem>
<!--<MenuItem Header="{DynamicResource Str_Experiment}"/>-->
</Menu>
@@ -76,10 +77,10 @@
</Border>
<Border Grid.Row="1">
<Grid>
<Grid x:Name="_rootGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="2.5*"/>
</Grid.ColumnDefinitions>
@@ -91,7 +92,7 @@
<!-- 模型列表页 -->
<TabItem Header="{DynamicResource Str_SpineObject}">
<Grid>
<Grid x:Name="_modelListGrid">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
@@ -147,13 +148,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}"
@@ -476,7 +480,7 @@
<ColumnDefinition Width="Auto" SharedSizeGroup="ColAniTime"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<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"/>
@@ -578,7 +582,7 @@
<!-- 浏览页 -->
<TabItem Header="{DynamicResource Str_Explorer}" DataContext="{Binding ExplorerListViewModel}">
<Grid>
<Grid x:Name="_explorerGrid">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
@@ -723,6 +727,8 @@
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 水平分辨率 -->
@@ -776,7 +782,15 @@
<TextBox Grid.Row="12" Grid.Column="1" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
<!-- 背景图案 -->
<Label Grid.Row="13" Grid.Column="0" Content="{DynamicResource Str_BackgroundImagePath}"/>
<DockPanel Grid.Row="13" Grid.Column="1" >
<Button DockPanel.Dock="Right" Content="..." Command="{Binding Cmd_SelectBackgroundImage}"/>
<TextBox Text="{Binding BackgroundImagePath}"/>
</DockPanel>
<!-- 背景图案模式 -->
<Label Grid.Row="14" Grid.Column="0" Content="{DynamicResource Str_BackgroundImageMode}"/>
<ComboBox Grid.Row="14" Grid.Column="1" SelectedValue="{Binding BackgroundImageMode}" ItemsSource="{Binding StretchOptions}"/>
</Grid>
</TabItem>
</TabControl>
@@ -785,14 +799,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"/>
@@ -918,6 +932,21 @@
Opened="BottomPopup_Opened"
MouseLeave="PopupContainer_MouseLeave"/>
</Grid>
<!-- 非可视元素通知栏图标 -->
<hc:NotifyIcon x:Name="_notifyIcon"
Icon="/Resources/Images/spineviewer.ico"
Click="_notifyIcon_Click"
MouseDoubleClick="_notifyIcon_MouseDoubleClick">
<hc:NotifyIcon.ContextMenu>
<ContextMenu>
<MenuItem Header="{DynamicResource Str_WallpaperView}" Command="{Binding Cmd_SwitchWallpaperView}" IsChecked="{Binding PreferenceViewModel.WallpaperView}"/>
<Separator/>
<MenuItem Header="{DynamicResource Str_Exit}" Command="{Binding Cmd_ExitFromTray}"/>
</ContextMenu>
</hc:NotifyIcon.ContextMenu>
</hc:NotifyIcon>
</Grid>
</Window>

View File

@@ -1,14 +1,19 @@
using Microsoft.Win32;
using NLog;
using NLog;
using NLog.Layouts;
using NLog.Targets;
using SFMLRenderer;
using Spine;
using SpineViewer.Models;
using SpineViewer.Natives;
using SpineViewer.Resources;
using SpineViewer.Services;
using SpineViewer.Utils;
using SpineViewer.ViewModels.MainWindow;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Reflection.Metadata;
using System.Text;
using System.Windows;
using System.Windows.Controls;
@@ -26,50 +31,48 @@ 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;
private readonly SFMLRenderWindow _wallpaperRenderWindow;
private readonly MainWindowViewModel _vm;
public MainWindow()
{
InitializeComponent();
InitializeLogConfiguration();
_vm = new (_renderPanel);
DataContext = _vm;
_vm.SFMLRendererViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging;
// Initialize Wallpaper RenderWindow
_wallpaperRenderWindow = new(new(1, 1), "SpineViewerWallpaper", SFML.Window.Styles.None);
_wallpaperRenderWindow.SetVisible(false);
var handle = _wallpaperRenderWindow.SystemHandle;
var style = User32.GetWindowLong(handle, User32.GWL_STYLE) | User32.WS_POPUP;
var exStyle = User32.GetWindowLong(handle, User32.GWL_EXSTYLE) | User32.WS_EX_LAYERED | User32.WS_EX_TOOLWINDOW;
User32.SetWindowLong(handle, User32.GWL_STYLE, style);
User32.SetWindowLong(handle, User32.GWL_EXSTYLE, exStyle);
User32.SetLayeredWindowAttributes(handle, 0, byte.MaxValue, User32.LWA_ALPHA);
DataContext = _vm = new(_renderPanel, _wallpaperRenderWindow);
// XXX: hc 的 NotifyIcon 的 Text 似乎没法双向绑定
_notifyIcon.Text = _vm.Title;
Loaded += MainWindow_Loaded;
ContentRendered += MainWindow_ContentRendered;
Closing += MainWindow_Closing;
Closed += MainWindow_Closed;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var vm = _vm.SFMLRendererViewModel;
_renderPanel.CanvasMouseWheelScrolled += vm.CanvasMouseWheelScrolled;
_renderPanel.CanvasMouseButtonPressed += vm.CanvasMouseButtonPressed;
_renderPanel.CanvasMouseMove += vm.CanvasMouseMove;
_renderPanel.CanvasMouseButtonReleased += vm.CanvasMouseButtonReleased;
_vm.SpineObjectListViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging;
_vm.SFMLRendererViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging;
// 设置默认参数并启动渲染
vm.ResolutionX = 1500;
vm.ResolutionY = 1000;
vm.Zoom = 0.75f;
vm.CenterX = 0;
vm.CenterY = 0;
vm.FlipY = true;
vm.MaxFps = 30;
vm.StartRender();
// 加载首选项
_vm.PreferenceViewModel.LoadPreference();
}
private void MainWindow_Closed(object? sender, EventArgs e)
{
var vm = _vm.SFMLRendererViewModel;
vm.StopRender();
_vm.SFMLRendererViewModel.PropertyChanged += SFMLRendererViewModel_PropertyChanged;
}
/// <summary>
@@ -81,14 +84,13 @@ public partial class MainWindow : Window
var rtbTarget = new NLog.Windows.Wpf.RichTextBoxTarget
{
Name = "rtbTarget",
FormName = GetType().Name,
WindowName = _mainWindow.Name,
ControlName = _loggerRichTextBox.Name,
AutoScroll = true,
MaxLines = 3000,
Layout = "[${level:format=OneLetter}]${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${message}"
Layout = "[${level:format=OneLetter}]${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${message}",
};
// TODO: 完善日志实现
rtbTarget.WordColoringRules.Add(new("[D]", "Gray", "Empty"));
rtbTarget.WordColoringRules.Add(new("[I]", "DimGray", "Empty"));
rtbTarget.WordColoringRules.Add(new("[W]", "DarkOrange", "Empty"));
@@ -100,6 +102,194 @@ 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, GridUnitType.Star);
_rootGrid.ColumnDefinitions[2].Width = new(m.RootGridCol2Width, GridUnitType.Star);
_modelListGrid.RowDefinitions[0].Height = new(m.ModelListRow0Height, GridUnitType.Star);
_modelListGrid.RowDefinitions[2].Height = new(m.ModelListRow2Height, GridUnitType.Star);
_explorerGrid.RowDefinitions[0].Height = new(m.ExplorerGridRow0Height, GridUnitType.Star);
_explorerGrid.RowDefinitions[2].Height = new(m.ExplorerGridRow2Height, GridUnitType.Star);
_rightPanelGrid.RowDefinitions[0].Height = new(m.RightPanelGridRow0Height, GridUnitType.Star);
_rightPanelGrid.RowDefinitions[2].Height = new(m.RightPanelGridRow2Height, GridUnitType.Star);
_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;
_vm.SFMLRendererViewModel.BackgroundImageMode = m.BackgroundImageMode;
}
}
private void SaveLastState()
{
var rb = RestoreBounds;
var m = new LastStateModel()
{
WindowLeft = rb.Left,
WindowTop = rb.Top,
WindowWidth = rb.Width,
WindowHeight = rb.Height,
WindowState = WindowState,
RootGridCol0Width = _rootGrid.ColumnDefinitions[0].Width.Value,
RootGridCol2Width = _rootGrid.ColumnDefinitions[2].Width.Value,
ModelListRow0Height = _modelListGrid.RowDefinitions[0].Height.Value,
ModelListRow2Height = _modelListGrid.RowDefinitions[2].Height.Value,
ExplorerGridRow0Height = _explorerGrid.RowDefinitions[0].Height.Value,
ExplorerGridRow2Height = _explorerGrid.RowDefinitions[2].Height.Value,
RightPanelGridRow0Height = _rightPanelGrid.RowDefinitions[0].Height.Value,
RightPanelGridRow2Height = _rightPanelGrid.RowDefinitions[2].Height.Value,
ResolutionX = _vm.SFMLRendererViewModel.ResolutionX,
ResolutionY = _vm.SFMLRendererViewModel.ResolutionY,
MaxFps = _vm.SFMLRendererViewModel.MaxFps,
Speed = _vm.SFMLRendererViewModel.Speed,
ShowAxis = _vm.SFMLRendererViewModel.ShowAxis,
BackgroundColor = _vm.SFMLRendererViewModel.BackgroundColor,
BackgroundImageMode = _vm.SFMLRendererViewModel.BackgroundImageMode,
};
JsonHelper.Serialize(m, LastStateFilePath);
}
#region MainWindow
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var vm = _vm.SFMLRendererViewModel;
_renderPanel.CanvasMouseWheelScrolled += vm.CanvasMouseWheelScrolled;
_renderPanel.CanvasMouseButtonPressed += (s, e) => { vm.CanvasMouseButtonPressed(s, e); _spinesListView.Focus(); }; // 用户点击画布后强制转移焦点至列表
_renderPanel.CanvasMouseMove += vm.CanvasMouseMove;
_renderPanel.CanvasMouseButtonReleased += vm.CanvasMouseButtonReleased;
// 设置默认参数并启动渲染
vm.SetResolution(1500, 1000);
vm.Zoom = 0.75f;
vm.CenterX = 0;
vm.CenterY = 0;
vm.FlipY = true;
vm.MaxFps = 30;
vm.StartRender();
// 加载首选项
_vm.PreferenceViewModel.LoadPreference();
LoadLastState();
}
private void MainWindow_ContentRendered(object? sender, EventArgs e)
{
string[] args = Environment.GetCommandLineArgs();
// 不带参数启动
if (args.Length <= 1)
return;
// 带一个参数启动, 允许提供一些启动选项
if (args.Length == 2)
{
if (args[1] == App.AutoRunFlag)
{
var autoPath = _vm.AutoRunWorkspaceConfigPath;
if (!string.IsNullOrWhiteSpace(autoPath) && JsonHelper.Deserialize<WorkspaceModel>(autoPath, out var obj))
_vm.Workspace = obj;
return;
}
}
// 其余提供了任意参数的情况
string[] filePaths = args.Skip(1).ToArray();
_vm.SpineObjectListViewModel.AddSpineObjectFromFileList(filePaths);
}
private void MainWindow_Closing(object? sender, CancelEventArgs e)
{
if (!_vm.IsShuttingDownFromTray)
{
if (_vm.CloseToTray is null)
{
_vm.PreferenceViewModel.CloseToTray = MessagePopupService.YesNo(AppResource.Str_CloseToTrayQuest);
_vm.PreferenceViewModel.SavePreference();
}
if (_vm.CloseToTray is true)
{
Hide();
e.Cancel = true;
return;
}
}
SaveLastState();
_vm.SFMLRendererViewModel.StopRender();
}
private void MainWindow_Closed(object? sender, EventArgs e)
{
}
#endregion
#region ViewModel PropertyChanged
private void SFMLRendererViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SFMLRendererViewModel.WallpaperView))
{
var wnd = _wallpaperRenderWindow;
if (_vm.SFMLRendererViewModel.WallpaperView)
{
var workerw = User32.GetWorkerW();
if (workerw == IntPtr.Zero)
{
_logger.Error("Failed to enable wallpaper view, WorkerW not found");
return;
}
var handle = wnd.SystemHandle;
User32.GetPrimaryScreenResolution(out var sw, out var sh);
User32.SetParent(handle, workerw);
User32.SetLayeredWindowAttributes(handle, 0, byte.MaxValue, User32.LWA_ALPHA);
_vm.SFMLRendererViewModel.SetResolution(sw, sh);
wnd.Position = new(0, 0);
wnd.Size = new(sw + 1, sh);
wnd.Size = new(sw, sh);
wnd.SetVisible(true);
}
else
{
wnd.SetVisible(false);
}
}
}
#endregion
#region _spinesListView
private void SpinesListView_RequestSelectionChanging(object? sender, NotifyCollectionChangedEventArgs e)
@@ -125,6 +315,9 @@ public partial class MainWindow : Window
default:
break;
}
// 如果选中项发生变化也强制转移焦点
_spinesListView.Focus();
}
private void SpinesListView_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
@@ -225,6 +418,36 @@ public partial class MainWindow : Window
#endregion
#region _spineFilesListBox
private void SpineFilesListBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var list = (ListBox)sender;
if (VisualUpwardSearch<ListBoxItem>(e.OriginalSource as DependencyObject) is null)
list.SelectedItems.Clear();
}
#endregion
#region _nofityIcon
private void _notifyIcon_Click(object sender, RoutedEventArgs e)
{
}
private void _notifyIcon_MouseDoubleClick(object sender, RoutedEventArgs e)
{
Show();
if (WindowState == WindowState.Minimized)
{
WindowState = WindowState.Normal;
}
Activate();
}
#endregion
#region
private void SwitchToFullScreenLayout()
@@ -234,11 +457,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);
@@ -485,10 +706,14 @@ public partial class MainWindow : Window
#endregion
private void SpineFilesListBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
private void DebugMenuItem_Click(object sender, RoutedEventArgs e)
{
var list = (ListBox)sender;
if (VisualUpwardSearch<ListBoxItem>(e.OriginalSource as DependencyObject) is null)
list.SelectedItems.Clear();
#if DEBUG
var a = _rootGrid.ColumnDefinitions[0].Width;
var b = _rootGrid.ColumnDefinitions[1].Width;
var c = _rootGrid.ColumnDefinitions[2].Width;
Debug.WriteLine(a);
Debug.WriteLine(_rootGrid.ColumnDefinitions[0].Width.IsStar);
#endif
}
}

View File

@@ -53,107 +53,229 @@
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Grid.IsSharedSizeScope="True">
<GroupBox Header="{DynamicResource Str_TextureLoadPreference}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ForcePremul}" ToolTip="{DynamicResource Str_ForcePremulTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding ForcePremul}" ToolTip="{DynamicResource Str_ForcePremulTooltip}"/>
</Grid>
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_ForcePremul}" ToolTip="{DynamicResource Str_ForcePremulTooltip}"/>
<ToggleButton Grid.Row="0" Grid.Column="1" IsChecked="{Binding ForcePremul}" ToolTip="{DynamicResource Str_ForcePremulTooltip}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ForceNearest}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding ForceNearest}"/>
</Grid>
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_ForceNearest}"/>
<ToggleButton Grid.Row="1" Grid.Column="1" IsChecked="{Binding ForceNearest}"/>
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_ForceMipmap}" ToolTip="{DynamicResource Str_ForceMipmapTooltip}"/>
<ToggleButton Grid.Row="2" Grid.Column="1" IsChecked="{Binding ForceMipmap}" ToolTip="{DynamicResource Str_ForceMipmapTooltip}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_ForceMipmap}" ToolTip="{DynamicResource Str_ForceMipmapTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding ForceMipmap}" ToolTip="{DynamicResource Str_ForceMipmapTooltip}"/>
</Grid>
</StackPanel>
</GroupBox>
<GroupBox Header="{DynamicResource Str_SpineLoadPreference}">
<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"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_IsShown}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding IsShown}"/>
</Grid>
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_IsShown}"/>
<ToggleButton Grid.Row="0" Grid.Column="1" IsChecked="{Binding IsShown}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_UsePma}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding UsePma}"/>
</Grid>
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_UsePma}"/>
<ToggleButton Grid.Row="1" Grid.Column="1" IsChecked="{Binding UsePma}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugTexture}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugTexture}"/>
</Grid>
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_DebugTexture}"/>
<ToggleButton Grid.Row="2" Grid.Column="1" IsChecked="{Binding DebugTexture}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugBounds}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugBounds}"/>
</Grid>
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_DebugBounds}"/>
<ToggleButton Grid.Row="3" Grid.Column="1" IsChecked="{Binding DebugBounds}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugBones}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugBones}"/>
</Grid>
<Label Grid.Row="4" Grid.Column="0" Content="{DynamicResource Str_DebugBones}"/>
<ToggleButton Grid.Row="4" Grid.Column="1" IsChecked="{Binding DebugBones}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugRegions}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugRegions}"/>
</Grid>
<Label Grid.Row="5" Grid.Column="0" Content="{DynamicResource Str_DebugRegions}"/>
<ToggleButton Grid.Row="5" Grid.Column="1" IsChecked="{Binding DebugRegions}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugMeshHulls}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugMeshHulls}"/>
</Grid>
<Label Grid.Row="6" Grid.Column="0" Content="{DynamicResource Str_DebugMeshHulls}"/>
<ToggleButton Grid.Row="6" Grid.Column="1" IsChecked="{Binding DebugMeshHulls}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugMeshes}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugMeshes}"/>
</Grid>
<Label Grid.Row="7" Grid.Column="0" Content="{DynamicResource Str_DebugMeshes}"/>
<ToggleButton Grid.Row="7" Grid.Column="1" IsChecked="{Binding DebugMeshes}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugClippings}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugClippings}"/>
</Grid>
<Label Grid.Row="8" Grid.Column="0" Content="{DynamicResource Str_DebugClippings}"/>
<ToggleButton Grid.Row="8" Grid.Column="1" IsChecked="{Binding DebugClippings}"/>
<!-- <Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugBoundingBoxes}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugBoundingBoxes}"/>
</Grid>
<!--<Label Grid.Row="9" Grid.Column="0" Content="{DynamicResource Str_DebugBoundingBoxes}"/>
<ToggleButton Grid.Row="9" Grid.Column="1" IsChecked="{Binding DebugBoundingBoxes}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugPaths}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugPaths}"/>
</Grid>
<Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_DebugPaths}"/>
<ToggleButton Grid.Row="10" Grid.Column="1" IsChecked="{Binding DebugPaths}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_DebugPoints}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugPoints}"/>
</Grid> -->
<Label Grid.Row="11" Grid.Column="0" Content="{DynamicResource Str_DebugPoints}"/>
<ToggleButton Grid.Row="11" Grid.Column="1" IsChecked="{Binding DebugPoints}"/>-->
</Grid>
</StackPanel>
</GroupBox>
<GroupBox Header="{DynamicResource Str_AppPreference}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_Language}"/>
<ComboBox Grid.Column="1"
SelectedItem="{Binding AppLanguage}"
ItemsSource="{x:Static vm:PreferenceViewModel.AppLanguageOptions}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_RenderSelectedOnly}"/>
<ToggleButton Grid.Column="1"
IsChecked="{Binding RenderSelectedOnly}"/>
</Grid>
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_RenderSelectedOnly}"/>
<ToggleButton Grid.Row="0" Grid.Column="1" IsChecked="{Binding RenderSelectedOnly}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_WallpaperView}"/>
<ToggleButton Grid.Column="1"
IsChecked="{Binding WallpaperView}"/>
</Grid>
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_Language}"/>
<ComboBox Grid.Row="1" Grid.Column="1"
SelectedItem="{Binding AppLanguage}"
ItemsSource="{x:Static vm:PreferenceViewModel.AppLanguageOptions}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_CloseToTray}"/>
<ToggleButton Grid.Column="1"
IsChecked="{Binding CloseToTray}"/>
</Grid>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_AutoRun}"/>
<ToggleButton Grid.Column="1"
IsChecked="{Binding AutoRun}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_AutoRunWorkspaceConfigPath}"
ToolTip="{DynamicResource Str_AutoRunWorkspaceConfigPathTooltip}"/>
<DockPanel Grid.Column="1" IsEnabled="{Binding AutoRun}">
<Button DockPanel.Dock="Right"
Content="..."
Command="{Binding Cmd_SelectAutoRunWorkspaceConfigPath}"/>
<TextBox Grid.Column="1"
Text="{Binding AutoRunWorkspaceConfigPath}"
ToolTip="{DynamicResource Str_AutoRunWorkspaceConfigPathTooltip}"/>
</DockPanel>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_AssociateFileSuffix}"/>
<ToggleButton Grid.Column="1"
IsChecked="{Binding AssociateFileSuffix}"/>
</Grid>
</StackPanel>
</GroupBox>
</StackPanel>
</ScrollViewer>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB