Compare commits

...

19 Commits

Author SHA1 Message Date
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
25 changed files with 537 additions and 412 deletions

View File

@@ -1,5 +1,18 @@
# CHANGELOG
## v0.15.18
- 完善窗口日志颜色标记
- 修复预览图背景颜色为透明
- 修复面板高度首次还原错误
- 增加托盘图标
- 增加可选预览背景画面和填充模式
- 增强支持的纹理格式(例如 webp
## v0.15.17
- 修改图标配色
## v0.15.16
- 修改模型添加顺序, 每次向顶层添加

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.15.18</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

@@ -28,6 +28,8 @@ A simple and user-friendly Spine file viewer and exporter with multi-language su
* Automatic resolution batch export.
* FFmpeg custom export support.
* Program parameter saving.
* File name extension association.
* Supports texture image formats other than PNG.
* ...
### Supported Spine Versions

View File

@@ -28,6 +28,8 @@
- 支持自动分辨率批量导出
- 支持 FFmpeg 自定义导出
- 支持程序参数保存
- 支持文件后缀关联
- 支持非 png 格式的纹理图片格式
- ......
### Spine 版本支持

View File

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

View File

@@ -1,4 +1,6 @@
using System;
using SFML.Graphics;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -40,37 +42,26 @@ namespace Spine.SpineWrappers
/// </summary>
public bool ForceMipmap { get; set; }
private SFML.Graphics.Texture ReadTexture(string path)
private Texture ReadTexture(string path)
{
if (ForcePremul)
{
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;
}
return new(path);
using var codec = SKCodec.Create(path, out var result);
if (codec is null || result != SKCodecResult.Success)
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)
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 +385,7 @@ namespace Spine.SpineWrappers
public virtual void Unload(object texture)
{
((SFML.Graphics.Texture)texture).Dispose();
((Texture)texture).Dispose();
}
}
}

View File

@@ -33,6 +33,8 @@ namespace SpineViewer.Models
public float Speed { get; set; } = 1f;
public bool ShowAxis { get; set; } = true;
public Color BackgroundColor { get; set; } = Color.FromRgb(105, 105, 105);
public string BackgroundImagePath { get; set; }
public Stretch BackgroundImageMode { get; set; } = Stretch.Uniform;
#endregion

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

@@ -14,10 +14,15 @@ namespace SpineViewer.Natives
/// </summary>
public static class Shell32
{
public const uint SHCNE_ASSOCCHANGED = 0x08000000;
public const uint SHCNF_IDLIST = 0x0000;
private const uint SHCNE_ASSOCCHANGED = 0x08000000;
private const uint SHCNF_IDLIST = 0x0000;
[DllImport("shell32.dll")]
public static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
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);
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -121,7 +121,8 @@
<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>

View File

@@ -121,7 +121,8 @@
<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>

View File

@@ -121,7 +121,8 @@
<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>

View File

@@ -90,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

@@ -7,7 +7,7 @@
<TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.15.16</Version>
<Version>0.15.18</Version>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
</PropertyGroup>
@@ -22,7 +22,9 @@
<Content Include="Resources\Images\skel.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Images\spineviewer.ico" />
<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++)
{

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
@@ -72,6 +73,8 @@ namespace SpineViewer.ViewModels.MainWindow
public SFMLRendererViewModel SFMLRendererViewModel => _sfmlRendererViewModel;
private readonly SFMLRendererViewModel _sfmlRendererViewModel;
public RelayCommand Cmd_Exit => new(App.Current.Shutdown);
/// <summary>
/// 打开工作区
/// </summary>

View File

@@ -310,7 +310,7 @@ namespace SpineViewer.ViewModels.MainWindow
Registry.CurrentUser.DeleteSubKeyTree($@"Software\Classes\{App.ProgId}", false);
}
Shell32.SHChangeNotify(Shell32.SHCNE_ASSOCCHANGED, Shell32.SHCNF_IDLIST, IntPtr.Zero, IntPtr.Zero);
Shell32.NotifyAssociationChanged();
});
}
}

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>
@@ -69,6 +73,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>
@@ -169,6 +180,64 @@ 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 RenderSelectedOnly
{
get => _renderSelectedOnly;
@@ -189,6 +258,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;
@@ -386,6 +463,38 @@ namespace SpineViewer.ViewModels.MainWindow
_renderer.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 (_showAxis)
{
// 画一个很长的坐标轴, 用 1e9 比较合适
@@ -442,7 +551,6 @@ namespace SpineViewer.ViewModels.MainWindow
public RendererWorkspaceConfigModel WorkspaceConfig
{
// TODO: 背景图片
get
{
return new()
@@ -459,6 +567,8 @@ namespace SpineViewer.ViewModels.MainWindow
Speed = Speed,
ShowAxis = ShowAxis,
BackgroundColor = BackgroundColor,
BackgroundImagePath = BackgroundImagePath,
BackgroundImageMode = BackgroundImageMode,
};
}
set
@@ -474,6 +584,8 @@ namespace SpineViewer.ViewModels.MainWindow
Speed = value.Speed;
ShowAxis = value.ShowAxis;
BackgroundColor = value.BackgroundColor;
BackgroundImagePath = value.BackgroundImagePath;
BackgroundImageMode = value.BackgroundImageMode;
}
}
}

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"
@@ -479,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"/>
@@ -726,6 +727,8 @@
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 水平分辨率 -->
@@ -779,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>
@@ -921,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="There may be a funtion :)" IsChecked="True"/>
<Separator/>
<MenuItem Header="{DynamicResource Str_Exit}" Command="{Binding Cmd_Exit}"/>
</ContextMenu>
</hc:NotifyIcon.ContextMenu>
</hc:NotifyIcon>
</Grid>
</Window>

View File

@@ -44,8 +44,9 @@ public partial class MainWindow : Window
{
InitializeComponent();
InitializeLogConfiguration();
_vm = new (_renderPanel);
DataContext = _vm;
DataContext = _vm = new(_renderPanel);
_notifyIcon.Text = _vm.Title; // XXX: hc 的 NotifyIcon 的 Text 似乎没法双向绑定
_vm.SpineObjectListViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging;
_vm.SFMLRendererViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging;
Loaded += MainWindow_Loaded;
@@ -111,14 +112,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"));
@@ -149,7 +149,7 @@ public partial class MainWindow : Window
_rootGrid.ColumnDefinitions[0].Width = new(m.RootGridCol0Width);
_modelListGrid.RowDefinitions[0].Height = new(m.ModelListRow0Height);
_explorerGrid.RowDefinitions[0].Height = new(m.ExplorerGridRow0Height);
if (m.ExplorerGridRow0Height > 0) _explorerGrid.RowDefinitions[0].Height = new(m.ExplorerGridRow0Height);
_rightPanelGrid.RowDefinitions[0].Height = new(m.RightPanelGridRow0Height);
_vm.SFMLRendererViewModel.SetResolution(m.ResolutionX, m.ResolutionY);
@@ -157,8 +157,9 @@ public partial class MainWindow : Window
_vm.SFMLRendererViewModel.Speed = m.Speed;
_vm.SFMLRendererViewModel.ShowAxis = m.ShowAxis;
_vm.SFMLRendererViewModel.BackgroundColor = m.BackgroundColor;
_vm.SFMLRendererViewModel.BackgroundImagePath = m.BackgroundImagePath;
_vm.SFMLRendererViewModel.BackgroundImageMode = m.BackgroundImageMode;
}
}
private void SaveLastState()
@@ -182,6 +183,8 @@ public partial class MainWindow : Window
Speed = _vm.SFMLRendererViewModel.Speed,
ShowAxis = _vm.SFMLRendererViewModel.ShowAxis,
BackgroundColor = _vm.SFMLRendererViewModel.BackgroundColor,
BackgroundImagePath = _vm.SFMLRendererViewModel.BackgroundImagePath,
BackgroundImageMode = _vm.SFMLRendererViewModel.BackgroundImageMode,
};
JsonHelper.Serialize(m, LastStateFilePath);
@@ -315,6 +318,31 @@ 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)
{
}
#endregion
#region
private void SwitchToFullScreenLayout()
@@ -572,11 +600,4 @@ public partial class MainWindow : Window
}
#endregion
private void SpineFilesListBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var list = (ListBox)sender;
if (VisualUpwardSearch<ListBoxItem>(e.OriginalSource as DependencyObject) is null)
list.SelectedItems.Clear();
}
}