From b59f228f2e719773d30025a53af96a34b0511ba3 Mon Sep 17 00:00:00 2001 From: ww-rm Date: Sun, 21 Sep 2025 11:01:58 +0800 Subject: [PATCH 01/13] refactor --- SpineViewer/Natives/Shell32.cs | 11 ++++++++--- .../ViewModels/MainWindow/PreferenceViewModel.cs | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/SpineViewer/Natives/Shell32.cs b/SpineViewer/Natives/Shell32.cs index a53286c..1a08aa6 100644 --- a/SpineViewer/Natives/Shell32.cs +++ b/SpineViewer/Natives/Shell32.cs @@ -14,10 +14,15 @@ namespace SpineViewer.Natives /// 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); + } } } diff --git a/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs b/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs index 950c304..a5e3adc 100644 --- a/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs +++ b/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs @@ -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(); }); } } From 8a4095dae1058057a09c970e5ef8241746146e84 Mon Sep 17 00:00:00 2001 From: ww-rm Date: Sun, 21 Sep 2025 22:06:00 +0800 Subject: [PATCH 02/13] =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RichTextBoxRowColoringRule.cs | 101 ++--- NLog.Windows.Wpf/RichTextBoxTarget.cs | 387 ++++++++++-------- .../RichTextBoxWordColoringRule.cs | 136 ++---- SpineViewer/Views/MainWindow.xaml | 1 + SpineViewer/Views/MainWindow.xaml.cs | 5 +- 5 files changed, 278 insertions(+), 352 deletions(-) diff --git a/NLog.Windows.Wpf/RichTextBoxRowColoringRule.cs b/NLog.Windows.Wpf/RichTextBoxRowColoringRule.cs index 9999a41..5bbe8c5 100644 --- a/NLog.Windows.Wpf/RichTextBoxRowColoringRule.cs +++ b/NLog.Windows.Wpf/RichTextBoxRowColoringRule.cs @@ -1,92 +1,53 @@ -// -// Copyright (c) 2004-2011 Jaroslaw Kowalski -// -// 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)); } } } diff --git a/NLog.Windows.Wpf/RichTextBoxTarget.cs b/NLog.Windows.Wpf/RichTextBoxTarget.cs index 04d905d..81e18c3 100644 --- a/NLog.Windows.Wpf/RichTextBoxTarget.cs +++ b/NLog.Windows.Wpf/RichTextBoxTarget.cs @@ -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 DefaultRowColoringRules { get; } = CreateDefaultColoringRules(); - static RichTextBoxTarget() + private static ReadOnlyCollection CreateDefaultColoringRules() { - var rules = new List() + return new List() { 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(); - RowColoringRules = new List(); - ToolWindow = true; - } - - private delegate void DelSendTheMessageToRichTextBox(string logMessage, RichTextBoxRowColoringRule rule); - - private delegate void FormCloseDelegate(); - - public static ReadOnlyCollection 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 RowColoringRules { get; private set; } - - [ArrayParameter(typeof(RichTextBoxWordColoringRule), "word-coloring")] - public IList 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 RowColoringRules { get; } = new List(); - internal RichTextBox TargetRichTextBox { get; set; } + [ArrayParameter(typeof(RichTextBoxWordColoringRule), "word-coloring")] + public IList WordColoringRules { get; } = new List(); - 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().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().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(); } } } -} +} \ No newline at end of file diff --git a/NLog.Windows.Wpf/RichTextBoxWordColoringRule.cs b/NLog.Windows.Wpf/RichTextBoxWordColoringRule.cs index b2e79c5..e577ad9 100644 --- a/NLog.Windows.Wpf/RichTextBoxWordColoringRule.cs +++ b/NLog.Windows.Wpf/RichTextBoxWordColoringRule.cs @@ -1,119 +1,59 @@ -// -// Copyright (c) 2004-2011 Jaroslaw Kowalski -// -// 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 WholeWords { get; set; } + public Layout 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; } } } diff --git a/SpineViewer/Views/MainWindow.xaml b/SpineViewer/Views/MainWindow.xaml index bda9156..0c5d959 100644 --- a/SpineViewer/Views/MainWindow.xaml +++ b/SpineViewer/Views/MainWindow.xaml @@ -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" diff --git a/SpineViewer/Views/MainWindow.xaml.cs b/SpineViewer/Views/MainWindow.xaml.cs index 3688730..d368096 100644 --- a/SpineViewer/Views/MainWindow.xaml.cs +++ b/SpineViewer/Views/MainWindow.xaml.cs @@ -111,14 +111,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")); From 72935d8f2b7ef08249720edffa3eecf7ca2f5c20 Mon Sep 17 00:00:00 2001 From: ww-rm Date: Sun, 21 Sep 2025 22:08:16 +0800 Subject: [PATCH 03/13] utf8 --- NLog.Windows.Wpf/NLog.Windows.Wpf.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NLog.Windows.Wpf/NLog.Windows.Wpf.csproj b/NLog.Windows.Wpf/NLog.Windows.Wpf.csproj index 69401ba..9a246f6 100644 --- a/NLog.Windows.Wpf/NLog.Windows.Wpf.csproj +++ b/NLog.Windows.Wpf/NLog.Windows.Wpf.csproj @@ -1,4 +1,4 @@ - + enable From 37235fa7d09253fd29e5199ab0ddd46c8768502b Mon Sep 17 00:00:00 2001 From: ww-rm Date: Mon, 22 Sep 2025 08:35:27 +0800 Subject: [PATCH 04/13] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E5=9B=BE=E8=83=8C=E6=99=AF=E9=A2=9C=E8=89=B2=E4=B8=BA=E9=80=8F?= =?UTF-8?q?=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SpineViewer/ViewModels/MainWindow/ExplorerListViewModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SpineViewer/ViewModels/MainWindow/ExplorerListViewModel.cs b/SpineViewer/ViewModels/MainWindow/ExplorerListViewModel.cs index 2eccb6c..1f836ba 100644 --- a/SpineViewer/ViewModels/MainWindow/ExplorerListViewModel.cs +++ b/SpineViewer/ViewModels/MainWindow/ExplorerListViewModel.cs @@ -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++) { From 0906817cd3c9e372e9ea6bb45c5663263383a1d3 Mon Sep 17 00:00:00 2001 From: ww-rm Date: Mon, 22 Sep 2025 14:38:04 +0800 Subject: [PATCH 05/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=E9=AB=98=E5=BA=A6=E9=A6=96=E6=AC=A1=E8=BF=98=E5=8E=9F=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SpineViewer/Views/MainWindow.xaml.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/SpineViewer/Views/MainWindow.xaml.cs b/SpineViewer/Views/MainWindow.xaml.cs index d368096..37b040d 100644 --- a/SpineViewer/Views/MainWindow.xaml.cs +++ b/SpineViewer/Views/MainWindow.xaml.cs @@ -148,7 +148,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,7 +157,6 @@ public partial class MainWindow : Window _vm.SFMLRendererViewModel.ShowAxis = m.ShowAxis; _vm.SFMLRendererViewModel.BackgroundColor = m.BackgroundColor; } - } private void SaveLastState() From 3e88e65bbd5951c3869b75efdef4b324c8982688 Mon Sep 17 00:00:00 2001 From: ww-rm Date: Mon, 22 Sep 2025 20:09:46 +0800 Subject: [PATCH 06/13] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=89=98=E7=9B=98?= =?UTF-8?q?=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SpineViewer/SpineViewer.csproj | 4 ++- .../MainWindow/MainWindowViewModel.cs | 3 ++ SpineViewer/Views/MainWindow.xaml | 17 ++++++++- SpineViewer/Views/MainWindow.xaml.cs | 36 ++++++++++++++----- 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/SpineViewer/SpineViewer.csproj b/SpineViewer/SpineViewer.csproj index 6802e32..be27fe0 100644 --- a/SpineViewer/SpineViewer.csproj +++ b/SpineViewer/SpineViewer.csproj @@ -22,7 +22,9 @@ PreserveNewest - + + PreserveNewest + diff --git a/SpineViewer/ViewModels/MainWindow/MainWindowViewModel.cs b/SpineViewer/ViewModels/MainWindow/MainWindowViewModel.cs index 31bc195..e844d31 100644 --- a/SpineViewer/ViewModels/MainWindow/MainWindowViewModel.cs +++ b/SpineViewer/ViewModels/MainWindow/MainWindowViewModel.cs @@ -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); + /// /// 打开工作区 /// diff --git a/SpineViewer/Views/MainWindow.xaml b/SpineViewer/Views/MainWindow.xaml index 0c5d959..1089639 100644 --- a/SpineViewer/Views/MainWindow.xaml +++ b/SpineViewer/Views/MainWindow.xaml @@ -480,7 +480,7 @@ - + diff --git a/SpineViewer/Views/MainWindow.xaml.cs b/SpineViewer/Views/MainWindow.xaml.cs index 37b040d..7792962 100644 --- a/SpineViewer/Views/MainWindow.xaml.cs +++ b/SpineViewer/Views/MainWindow.xaml.cs @@ -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; @@ -313,6 +314,31 @@ public partial class MainWindow : Window #endregion + #region _spineFilesListBox 事件 + + private void SpineFilesListBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + var list = (ListBox)sender; + if (VisualUpwardSearch(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() @@ -571,10 +597,4 @@ public partial class MainWindow : Window #endregion - private void SpineFilesListBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) - { - var list = (ListBox)sender; - if (VisualUpwardSearch(e.OriginalSource as DependencyObject) is null) - list.SelectedItems.Clear(); - } } \ No newline at end of file From 798883d4e0df1d161920e6c6c3912a525e464820 Mon Sep 17 00:00:00 2001 From: ww-rm Date: Mon, 22 Sep 2025 23:23:01 +0800 Subject: [PATCH 07/13] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=83=8C=E6=99=AF?= =?UTF-8?q?=E5=9B=BE=E6=A1=88=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SpineViewer/Models/LastStateModel.cs | 2 + SpineViewer/Resources/Strings/en.xaml | 3 +- SpineViewer/Resources/Strings/ja.xaml | 3 +- SpineViewer/Resources/Strings/zh.xaml | 3 +- SpineViewer/Services/DialogService.cs | 16 +++ .../MainWindow/SFMLRendererViewModel.cs | 107 ++++++++++++++++++ SpineViewer/Views/MainWindow.xaml | 10 ++ SpineViewer/Views/MainWindow.xaml.cs | 5 +- 8 files changed, 145 insertions(+), 4 deletions(-) diff --git a/SpineViewer/Models/LastStateModel.cs b/SpineViewer/Models/LastStateModel.cs index c61455f..639132a 100644 --- a/SpineViewer/Models/LastStateModel.cs +++ b/SpineViewer/Models/LastStateModel.cs @@ -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 diff --git a/SpineViewer/Resources/Strings/en.xaml b/SpineViewer/Resources/Strings/en.xaml index f77be99..7cdda02 100644 --- a/SpineViewer/Resources/Strings/en.xaml +++ b/SpineViewer/Resources/Strings/en.xaml @@ -121,7 +121,8 @@ Render Selected Only Show Axis Background Color - Background Image + Background Image Path + Background Image Mode Stop diff --git a/SpineViewer/Resources/Strings/ja.xaml b/SpineViewer/Resources/Strings/ja.xaml index 6a84772..5f4ef84 100644 --- a/SpineViewer/Resources/Strings/ja.xaml +++ b/SpineViewer/Resources/Strings/ja.xaml @@ -121,7 +121,8 @@ 選択のみレンダリング 座標軸を表示 背景色 - 背景画像 + 背景画像のパス + 背景画像のモード 停止 diff --git a/SpineViewer/Resources/Strings/zh.xaml b/SpineViewer/Resources/Strings/zh.xaml index f138b4c..793fd7e 100644 --- a/SpineViewer/Resources/Strings/zh.xaml +++ b/SpineViewer/Resources/Strings/zh.xaml @@ -121,7 +121,8 @@ 仅渲染选中 显示坐标轴 背景颜色 - 背景图片 + 背景图片路径 + 背景图片模式 停止 diff --git a/SpineViewer/Services/DialogService.cs b/SpineViewer/Services/DialogService.cs index 6e11135..7587af2 100644 --- a/SpineViewer/Services/DialogService.cs +++ b/SpineViewer/Services/DialogService.cs @@ -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() diff --git a/SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs b/SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs index 348c80e..7dc9751 100644 --- a/SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs +++ b/SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs @@ -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 StretchOptions { get; } = Enum.GetValues().ToImmutableArray(); + /// /// 日志器 /// @@ -69,6 +73,13 @@ namespace SpineViewer.ViewModels.MainWindow private float _forwardDelta = 0; private readonly object _forwardDeltaLock = new(); + /// + /// 背景图片 + /// + private SFML.Graphics.Sprite? _backgroundImageSprite; // XXX: 暂时未使用 Dispose 释放 + private SFML.Graphics.Texture? _backgroundImageTexture; // XXX: 暂时未使用 Dispose 释放 + private readonly object _bgLock = new(); + /// /// 临时变量, 记录拖放世界源点 /// @@ -169,6 +180,62 @@ 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; + } + 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 +256,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 +461,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 比较合适 diff --git a/SpineViewer/Views/MainWindow.xaml b/SpineViewer/Views/MainWindow.xaml index 1089639..965473a 100644 --- a/SpineViewer/Views/MainWindow.xaml +++ b/SpineViewer/Views/MainWindow.xaml @@ -727,6 +727,8 @@ + + @@ -780,7 +782,15 @@ +