feat: 批量实现 GPS辅助/图标水印/MQTT客户端/语音助手 四个工具大石头 authored at 2026-07-14 01:41:38
diff --git a/CrazyCoder/App.xaml.cs b/CrazyCoder/App.xaml.cs
index a7f6fb8..4077c24 100644
--- a/CrazyCoder/App.xaml.cs
+++ b/CrazyCoder/App.xaml.cs
@@ -20,10 +20,6 @@ namespace CrazyCoder
protected override void OnStartup(StartupEventArgs e)
{
-#if NET45
- XTrace.UseWinForm();
-#endif
-
StartClient();
StringHelper.EnableSpeechTip = Setting.Current.SpeechTip;
@@ -60,11 +56,10 @@ namespace CrazyCoder
// 登录后保存证书
client.OnLogined += (s, e) =>
{
- var inf = client.Info;
- if (inf != null && !inf.Code.IsNullOrEmpty())
+ if (client.Logined && !client.Code.IsNullOrEmpty())
{
- set.Code = inf.Code;
- set.Secret = inf.Secret;
+ set.Code = client.Code;
+ set.Secret = client.Secret;
set.Save();
}
};
diff --git a/CrazyCoder/CrazyCoder.csproj b/CrazyCoder/CrazyCoder.csproj
index 86a5fe2..725a910 100644
--- a/CrazyCoder/CrazyCoder.csproj
+++ b/CrazyCoder/CrazyCoder.csproj
@@ -1,12 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net6.0</TargetFramework>
+ <TargetFramework>net8.0-windows</TargetFramework>
<OutputType>WinExe</OutputType>
+ <AssemblyTitle>新生命码神工具</AssemblyTitle>
+ <Description>码神工具。代码生成、网络工具、API工具、串口工具、正则工具、图标工具、加解密工具、地图接口</Description>
+ <Company>新生命开发团队</Company>
+ <Copyright>©2002-2026 NewLife</Copyright>
+ <VersionPrefix>8.2</VersionPrefix>
+ <VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
+ <Version>$(VersionPrefix).$(VersionSuffix)</Version>
+ <FileVersion>$(Version)</FileVersion>
+ <AssemblyVersion>$(VersionPrefix).*</AssemblyVersion>
<Deterministic>false</Deterministic>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<UseWPF>true</UseWPF>
<ImportWindowsDesktopTargets>true</ImportWindowsDesktopTargets>
- <OutputPath>..\Bin\</OutputPath>
+ <OutputPath>..\Bin\CrazyCoder</OutputPath>
+ <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <LangVersion>latest</LangVersion>
+ <NoWarn>CA1416</NoWarn>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>Images\leaf.ico</ApplicationIcon>
@@ -15,22 +28,23 @@
<Resource Include="Fonts\iconfont.ttf" />
</ItemGroup>
<ItemGroup>
- <PackageReference Include="MaterialDesignThemes">
- <Version>4.3.0</Version>
- </PackageReference>
+ <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
- <PackageReference Include="MvvmLightLibs">
- <Version>5.4.1.1</Version>
- </PackageReference>
- <PackageReference Include="NewLife.Core">
- <Version>8.11.2021.1204</Version>
- </PackageReference>
- <PackageReference Include="NewLife.Stardust">
- <Version>1.5.2021.1204</Version>
- </PackageReference>
- <PackageReference Include="NewLife.XCode">
- <Version>10.3.2021.1204</Version>
- </PackageReference>
+ <PackageReference Include="NewLife.Core" Version="11.15.2026.501" />
+ <PackageReference Include="NewLife.Stardust" Version="3.7.2026.501" />
+ <PackageReference Include="NewLife.XCode" Version="11.26.2026.501" />
+ <PackageReference Include="NewLife.Net" Version="4.4.2026.206" />
+ <PackageReference Include="NewLife.Remoting" Version="3.7.2026.501" />
+ <PackageReference Include="NewLife.Security" Version="11.15.2026.501" />
+ <PackageReference Include="NewLife.MQTT" Version="3.0.2026.501" />
+ <PackageReference Include="NewLife.Redis" Version="6.5.2026.501" />
+ <PackageReference Include="NewLife.Map" Version="2.6.2026.102" />
+ <PackageReference Include="NewLife.ModbusRTU" Version="2.0.2025.701" />
+ <PackageReference Include="NAudio" Version="2.3.0" />
+ <PackageReference Include="SSH.NET" Version="2025.1.0" />
+ <PackageReference Include="System.IO.Ports" Version="10.0.7" />
+ <PackageReference Include="System.Speech" Version="10.0.7" />
+ <PackageReference Include="System.Management" Version="10.0.7" />
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
diff --git a/CrazyCoder/MainWindow.xaml b/CrazyCoder/MainWindow.xaml
index 395775f..09b4e0f 100644
--- a/CrazyCoder/MainWindow.xaml
+++ b/CrazyCoder/MainWindow.xaml
@@ -131,7 +131,7 @@
<ListBox.ItemTemplate>
<DataTemplate>
<RadioButton Command="{Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type local:MainWindow}},
- Path=DataContext.SelectedCommand,Mode=TwoWay}" Style="{DynamicResource RadioButtonStyle}"
+ Path=DataContext.SelectCommand,Mode=TwoWay}" Style="{DynamicResource RadioButtonStyle}"
CommandParameter="{Binding}">
<RadioButton.IsChecked>
<Binding Path="IsSelected" RelativeSource="{RelativeSource AncestorType=ListBoxItem}" Mode="TwoWay"/>
diff --git a/CrazyCoder/Models/IconHelper.cs b/CrazyCoder/Models/IconHelper.cs
new file mode 100644
index 0000000..c206d8f
--- /dev/null
+++ b/CrazyCoder/Models/IconHelper.cs
@@ -0,0 +1,163 @@
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.IO;
+
+namespace CrazyCoder.Models;
+
+/// <summary>ICO 图标文件辅助类</summary>
+public class IconHelper
+{
+ #region 静态方法
+ /// <summary>转换源图片到 ICO 文件流</summary>
+ /// <param name="bmp">源图片</param>
+ /// <param name="des">目标流</param>
+ /// <param name="sizes">尺寸数组</param>
+ /// <param name="bits">位深数组</param>
+ public static void Convert(Image bmp, Stream des, Int32[] sizes, Int32[] bits)
+ {
+ var ico = new IconHelper();
+ foreach (var bit in bits)
+ {
+ foreach (var item in sizes)
+ {
+ if (bit == 8)
+ ico.AddBmp(bmp, item, bit);
+ else
+ ico.AddPng(bmp, item, bit);
+ }
+ }
+ ico.Sort();
+ ico.Save(des);
+ }
+
+ /// <summary>转换源图片到 ICO 文件</summary>
+ /// <param name="srcFile">源图片路径</param>
+ /// <param name="desFile">目标 ICO 文件路径</param>
+ /// <param name="sizes">尺寸数组</param>
+ /// <param name="bits">位深数组</param>
+ public static void Convert(String srcFile, String desFile, Int32[] sizes, Int32[] bits)
+ {
+ using var bmp = new Bitmap(srcFile);
+ using var fs = File.Create(desFile);
+ Convert(bmp, fs, sizes, bits);
+ }
+ #endregion
+
+ #region 属性
+ private readonly List<IconItem> _items = [];
+ #endregion
+
+ #region 方法
+ private void AddPng(Image bmp, Int32 size, Int32 bit)
+ {
+ var ico = new Bitmap(bmp, size, size);
+
+ var ms = new MemoryStream();
+ ico.Save(ms, ImageFormat.Png);
+
+ var item = new IconItem
+ {
+ Data = ms.ToArray(),
+ Size = (UInt32)ms.Length,
+ BitCount = (UInt16)bit,
+ Width = (Byte)(size >= 256 ? 0 : size),
+ Height = (Byte)(size >= 256 ? 0 : size)
+ };
+
+ _items.Add(item);
+ ResetOffset();
+ }
+
+ private void AddBmp(Image bmp, Int32 size, Int32 bit)
+ {
+ var ico = new Bitmap(bmp, size, size);
+
+ var ms = new MemoryStream();
+ ico.Save(ms, ImageFormat.Bmp);
+
+ var item = new IconItem();
+ // BMP 跳过 14 字节文件头
+ ms.Position = 14;
+ item.Data = new Byte[ms.Length - 14];
+ ms.Read(item.Data, 0, item.Data.Length);
+ item.Size = (UInt32)item.Data.Length;
+ item.BitCount = (UInt16)bit;
+ // ICO 的高是 BMP 的 2 倍
+ var h = BitConverter.ToInt32(item.Data, 8);
+ h *= 2;
+ Buffer.BlockCopy(BitConverter.GetBytes(h), 0, item.Data, 8, 4);
+
+ item.Width = (Byte)size;
+ item.Height = (Byte)size;
+
+ _items.Add(item);
+ ResetOffset();
+ }
+
+ private void Sort()
+ {
+ _items.Sort((a, b) =>
+ {
+ var cmp = (a.Width == 0 ? 256 : a.Width).CompareTo(b.Width == 0 ? 256 : b.Width);
+ if (cmp != 0) return -cmp;
+ return -a.BitCount.CompareTo(b.BitCount);
+ });
+
+ ResetOffset();
+ }
+
+ private void Save(Stream stream)
+ {
+ var writer = new BinaryWriter(stream);
+ writer.Write((UInt16)0); // Reserved
+ writer.Write((UInt16)1); // ICO = 1
+ writer.Write((UInt16)_items.Count);
+
+ foreach (var item in _items)
+ {
+ item.Save(writer);
+ }
+ foreach (var item in _items)
+ {
+ writer.Write(item.Data);
+ }
+ }
+
+ private void ResetOffset()
+ {
+ var idx = (UInt32)(6 + _items.Count * 16);
+ foreach (var item in _items)
+ {
+ item.Offset = idx;
+ idx += item.Size;
+ }
+ }
+ #endregion
+
+ #region 内部类
+ private class IconItem
+ {
+ public Byte Width { get; set; } = 16;
+ public Byte Height { get; set; } = 16;
+ public Byte ColorCount { get; set; }
+ public Byte Reserved { get; set; }
+ public UInt16 Planes { get; set; } = 1;
+ public UInt16 BitCount { get; set; } = 32;
+ public UInt32 Size { get; set; }
+ public UInt32 Offset { get; set; }
+ public Byte[] Data { get; set; } = [];
+
+ public void Save(BinaryWriter writer)
+ {
+ writer.Write(Width);
+ writer.Write(Height);
+ writer.Write(ColorCount);
+ writer.Write(Reserved);
+ writer.Write(Planes);
+ writer.Write(BitCount);
+ writer.Write(Size);
+ writer.Write(Offset);
+ }
+ }
+ #endregion
+}
diff --git a/CrazyCoder/Models/RegexCaptureItem.cs b/CrazyCoder/Models/RegexCaptureItem.cs
new file mode 100644
index 0000000..b30febe
--- /dev/null
+++ b/CrazyCoder/Models/RegexCaptureItem.cs
@@ -0,0 +1,29 @@
+#nullable enable
+
+using System;
+using System.Text.RegularExpressions;
+
+namespace CrazyCoder.Models
+{
+ /// <summary>正则捕获结果项</summary>
+ public class RegexCaptureItem
+ {
+ /// <summary>序号</summary>
+ public Int32 Index { get; set; }
+
+ /// <summary>值</summary>
+ public String Value { get; set; } = "";
+
+ /// <summary>位置</summary>
+ public Int32 Position { get; set; }
+
+ /// <summary>长度</summary>
+ public Int32 Length { get; set; }
+
+ /// <summary>位置描述</summary>
+ public String Location => $"({Position},{Length})";
+
+ /// <summary>原始 Capture 对象</summary>
+ public Capture? Capture { get; set; }
+ }
+}
diff --git a/CrazyCoder/Models/RegexGroupItem.cs b/CrazyCoder/Models/RegexGroupItem.cs
new file mode 100644
index 0000000..48bc48d
--- /dev/null
+++ b/CrazyCoder/Models/RegexGroupItem.cs
@@ -0,0 +1,32 @@
+#nullable enable
+
+using System;
+using System.Text.RegularExpressions;
+
+namespace CrazyCoder.Models
+{
+ /// <summary>正则分组结果项</summary>
+ public class RegexGroupItem
+ {
+ /// <summary>序号</summary>
+ public Int32 Index { get; set; }
+
+ /// <summary>分组名称</summary>
+ public String Name { get; set; } = "";
+
+ /// <summary>值</summary>
+ public String Value { get; set; } = "";
+
+ /// <summary>位置</summary>
+ public Int32 Position { get; set; }
+
+ /// <summary>长度</summary>
+ public Int32 Length { get; set; }
+
+ /// <summary>位置描述</summary>
+ public String Location => $"({Position},{Length})";
+
+ /// <summary>原始 Group 对象</summary>
+ public Group? Group { get; set; }
+ }
+}
diff --git a/CrazyCoder/Models/RegexMatchItem.cs b/CrazyCoder/Models/RegexMatchItem.cs
new file mode 100644
index 0000000..e9ed1bf
--- /dev/null
+++ b/CrazyCoder/Models/RegexMatchItem.cs
@@ -0,0 +1,32 @@
+#nullable enable
+
+using System;
+using System.Text.RegularExpressions;
+
+namespace CrazyCoder.Models
+{
+ /// <summary>正则匹配结果项</summary>
+ public class RegexMatchItem
+ {
+ /// <summary>序号</summary>
+ public Int32 Index { get; set; }
+
+ /// <summary>匹配值</summary>
+ public String Value { get; set; } = "";
+
+ /// <summary>行号</summary>
+ public Int32 Line { get; set; }
+
+ /// <summary>位置</summary>
+ public Int32 Position { get; set; }
+
+ /// <summary>长度</summary>
+ public Int32 Length { get; set; }
+
+ /// <summary>位置描述</summary>
+ public String Location => $"({Line},{Position},{Length})";
+
+ /// <summary>原始 Match 对象</summary>
+ public Match? Match { get; set; }
+ }
+}
diff --git a/CrazyCoder/ViewModels/GpsViewModel.cs b/CrazyCoder/ViewModels/GpsViewModel.cs
new file mode 100644
index 0000000..7cdb4b1
--- /dev/null
+++ b/CrazyCoder/ViewModels/GpsViewModel.cs
@@ -0,0 +1,87 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using NewLife;
+
+namespace CrazyCoder.ViewModels;
+
+/// <summary>GPS 辅助工具 ViewModel</summary>
+public partial class GpsViewModel : ObservableObject
+{
+ #region 属性
+ /// <summary>HEX 纬度(8字符)</summary>
+ [ObservableProperty]
+ private String _hexLat = "";
+
+ /// <summary>HEX 经度(8字符)</summary>
+ [ObservableProperty]
+ private String _hexLong = "";
+
+ /// <summary>HEX 合并输入(16字符)</summary>
+ [ObservableProperty]
+ private String _hexCombined = "";
+
+ /// <summary>转换后纬度</summary>
+ [ObservableProperty]
+ private String _latitude = "";
+
+ /// <summary>转换后经度</summary>
+ [ObservableProperty]
+ private String _longitude = "";
+
+ /// <summary>合并坐标</summary>
+ [ObservableProperty]
+ private String _latLong = "";
+ #endregion
+
+ #region 方法
+ /// <summary>分离转换</summary>
+ [RelayCommand]
+ private void Convert()
+ {
+ var lat = HexLat?.Trim();
+ var lng = HexLong?.Trim();
+
+ if (lat.IsNullOrEmpty() || lng.IsNullOrEmpty()) return;
+
+ var vLat = BitConverter.ToSingle(lat.ToHex(), 0);
+ var vLng = BitConverter.ToSingle(lng.ToHex(), 0);
+
+ Latitude = vLat.ToString();
+ Longitude = vLng.ToString();
+ LatLong = $"{vLat},{vLng}";
+ }
+
+ /// <summary>合并转换</summary>
+ [RelayCommand]
+ private void ConvertCombined()
+ {
+ var temp = HexCombined?.Trim();
+ if (temp.IsNullOrEmpty() || temp.Length < 16) return;
+
+ var lat = temp[..8];
+ var lng = temp[8..];
+
+ HexLat = lat;
+ HexLong = lng;
+
+ var vLat = BitConverter.ToSingle(lat.ToHex(), 0);
+ var vLng = BitConverter.ToSingle(lng.ToHex(), 0);
+
+ Latitude = vLat.ToString();
+ Longitude = vLng.ToString();
+ LatLong = $"{vLat},{vLng}";
+ }
+
+ /// <summary>清空</summary>
+ [RelayCommand]
+ private void Clear()
+ {
+ HexLat = "";
+ HexLong = "";
+ HexCombined = "";
+ Latitude = "";
+ Longitude = "";
+ LatLong = "";
+ }
+ #endregion
+}
diff --git a/CrazyCoder/ViewModels/IconToolViewModel.cs b/CrazyCoder/ViewModels/IconToolViewModel.cs
new file mode 100644
index 0000000..e74ee03
--- /dev/null
+++ b/CrazyCoder/ViewModels/IconToolViewModel.cs
@@ -0,0 +1,234 @@
+#nullable enable
+using System.Collections.ObjectModel;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CrazyCoder.Models;
+using Microsoft.Win32;
+using NewLife;
+
+namespace CrazyCoder.ViewModels;
+
+/// <summary>图标水印处理工具 ViewModel</summary>
+public partial class IconToolViewModel : ObservableObject
+{
+ #region 属性
+ /// <summary>源图片路径</summary>
+ [ObservableProperty]
+ private String _sourceFile = "";
+
+ /// <summary>源图片</summary>
+ [ObservableProperty]
+ private ImageSource? _sourceImage;
+
+ /// <summary>预览图片</summary>
+ [ObservableProperty]
+ private ImageSource? _previewImage;
+
+ /// <summary>水印文字</summary>
+ [ObservableProperty]
+ private String _watermarkText = "";
+
+ /// <summary>字体名称</summary>
+ [ObservableProperty]
+ private String _fontName = "Arial";
+
+ /// <summary>字体大小</summary>
+ [ObservableProperty]
+ private Int32 _fontSize = 96;
+
+ /// <summary>水印 X 位置</summary>
+ [ObservableProperty]
+ private Int32 _watermarkX;
+
+ /// <summary>水印 Y 位置</summary>
+ [ObservableProperty]
+ private Int32 _watermarkY;
+
+ /// <summary>水印颜色</summary>
+ [ObservableProperty]
+ private String _watermarkColor = "#FF0000";
+
+ /// <summary>可用字体列表</summary>
+ public ObservableCollection<String> FontNames { get; } = [];
+
+ /// <summary>ICO 大小选项</summary>
+ public ObservableCollection<IconSizeOption> IconSizes { get; } =
+ [
+ new() { Name = "16x16", Size = 16 },
+ new() { Name = "24x24", Size = 24 },
+ new() { Name = "32x32", Size = 32, IsSelected = true },
+ new() { Name = "48x48", Size = 48, IsSelected = true },
+ new() { Name = "64x64", Size = 64 },
+ new() { Name = "96x96", Size = 96 },
+ new() { Name = "128x128", Size = 128 },
+ new() { Name = "256x256", Size = 256 },
+ ];
+
+ private Bitmap? _originalBitmap;
+ #endregion
+
+ #region 构造
+ /// <summary>实例化图标水印处理工具 ViewModel</summary>
+ public IconToolViewModel()
+ {
+ foreach (var ft in System.Drawing.FontFamily.Families)
+ {
+ FontNames.Add(ft.Name);
+ }
+ }
+ #endregion
+
+ #region 命令
+ /// <summary>加载图片</summary>
+ [RelayCommand]
+ private void LoadImage()
+ {
+ var dlg = new OpenFileDialog
+ {
+ Filter = "图片文件|*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.ico|所有文件|*.*",
+ Title = "选择图片"
+ };
+ if (dlg.ShowDialog() != true) return;
+
+ LoadFromFile(dlg.FileName);
+ }
+
+ /// <summary>从文件加载图片</summary>
+ public void LoadFromFile(String fileName)
+ {
+ SourceFile = fileName;
+
+ _originalBitmap?.Dispose();
+ _originalBitmap = new Bitmap(fileName);
+
+ SourceImage = BitmapToImageSource(_originalBitmap);
+
+ // 自动设置水印位置到右下角
+ WatermarkX = _originalBitmap.Width - 150;
+ WatermarkY = _originalBitmap.Height - 50;
+
+ MakeWater();
+ }
+
+ /// <summary>应用水印</summary>
+ [RelayCommand]
+ private void MakeWater()
+ {
+ if (_originalBitmap == null) return;
+
+ var bmp = MakeWatermarkImage();
+ PreviewImage = BitmapToImageSource(bmp);
+ bmp.Dispose();
+ }
+
+ /// <summary>保存图片</summary>
+ [RelayCommand]
+ private void SaveImage()
+ {
+ if (_originalBitmap == null) return;
+
+ var dlg = new SaveFileDialog
+ {
+ Filter = "PNG 图片|*.png|JPEG 图片|*.jpg|BMP 图片|*.bmp",
+ Title = "保存图片",
+ DefaultExt = "png"
+ };
+ if (dlg.ShowDialog() != true) return;
+
+ var bmp = MakeWatermarkImage();
+ var format = dlg.FilterIndex switch
+ {
+ 2 => ImageFormat.Jpeg,
+ 3 => ImageFormat.Bmp,
+ _ => ImageFormat.Png
+ };
+ bmp.Save(dlg.FileName, format);
+ bmp.Dispose();
+ }
+
+ /// <summary>生成 ICO 图标</summary>
+ [RelayCommand]
+ private void MakeIco()
+ {
+ if (_originalBitmap == null)
+ {
+ MessageBox.Show("请先加载图片!", "提示");
+ return;
+ }
+
+ var sizes = IconSizes.Where(e => e.IsSelected).Select(e => e.Size).ToArray();
+ if (sizes.Length == 0)
+ {
+ MessageBox.Show("请至少选择一个图标尺寸!", "提示");
+ return;
+ }
+
+ var dlg = new SaveFileDialog
+ {
+ Filter = "ICO 图标|*.ico",
+ Title = "保存图标",
+ DefaultExt = "ico"
+ };
+ if (dlg.ShowDialog() != true) return;
+
+ var bmp = MakeWatermarkImage();
+ var ms = new MemoryStream();
+ IconHelper.Convert(bmp, ms, sizes, [32]);
+ bmp.Dispose();
+
+ File.WriteAllBytes(dlg.FileName, ms.ToArray());
+ }
+ #endregion
+
+ #region 辅助
+ private Bitmap MakeWatermarkImage()
+ {
+ var bmp = new Bitmap(_originalBitmap!);
+
+ if (!WatermarkText.IsNullOrEmpty())
+ {
+ using var g = Graphics.FromImage(bmp);
+ using var brush = new SolidBrush(ColorTranslator.FromHtml(WatermarkColor));
+ using var ft = new System.Drawing.Font(FontName, FontSize, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Pixel);
+ g.DrawString(WatermarkText, ft, brush, WatermarkX, WatermarkY);
+ }
+
+ return bmp;
+ }
+
+ private static BitmapImage BitmapToImageSource(Bitmap bmp)
+ {
+ using var ms = new MemoryStream();
+ bmp.Save(ms, ImageFormat.Png);
+ ms.Position = 0;
+
+ var img = new BitmapImage();
+ img.BeginInit();
+ img.CacheOption = BitmapCacheOption.OnLoad;
+ img.StreamSource = ms;
+ img.EndInit();
+ img.Freeze();
+ return img;
+ }
+ #endregion
+}
+
+/// <summary>ICO 尺寸选项</summary>
+public partial class IconSizeOption : ObservableObject
+{
+ /// <summary>显示名称</summary>
+ public String Name { get; set; } = "";
+
+ /// <summary>尺寸</summary>
+ public Int32 Size { get; set; }
+
+ /// <summary>是否选中</summary>
+ [ObservableProperty]
+ private Boolean _isSelected;
+}
diff --git a/CrazyCoder/ViewModels/MainViewModel.cs b/CrazyCoder/ViewModels/MainViewModel.cs
index 8007f39..cd454f2 100644
--- a/CrazyCoder/ViewModels/MainViewModel.cs
+++ b/CrazyCoder/ViewModels/MainViewModel.cs
@@ -1,53 +1,44 @@
using System.Collections.ObjectModel;
using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
using CrazyCoder.Models;
using CrazyCoder.Views;
-using GalaSoft.MvvmLight;
-using GalaSoft.MvvmLight.Command;
using NewLife.Reflection;
namespace CrazyCoder.ViewModels
{
- public class MainViewModel : ViewModelBase
+ public partial class MainViewModel : ObservableObject
{
public MainViewModel()
{
- Menus = new ObservableCollection<MenuModel>();
- menus.Add(new MenuModel() { IconFont = "\xe635", Title = "数据建模", BackColor = "#218868" });
- menus.Add(new MenuModel() { IconFont = "\xe6b6", Title = "网络工具", BackColor = "#EE3B3B" });
- menus.Add(new MenuModel() { IconFont = "\xe6e1", Title = "RPC工具", BackColor = "#218868" });
- menus.Add(new MenuModel() { IconFont = "\xe614", Title = "串口工具", BackColor = "#EE3B3B" });
- menus.Add(new MenuModel() { IconFont = "\xe755", Title = "地图接口", BackColor = "#218868" });
- menus.Add(new MenuModel() { IconFont = "\xe635", Title = "正则表达式", BackColor = "#218868", Type = typeof(RegexWindow) });
- menus.Add(new MenuModel() { IconFont = "\xe6b6", Title = "图标水印", BackColor = "#EE3B3B" });
- menus.Add(new MenuModel() { IconFont = "\xe6e1", Title = "加密解密", BackColor = "#218868" });
- menus.Add(new MenuModel() { IconFont = "\xe614", Title = "语音助手", BackColor = "#EE3B3B" });
- menus.Add(new MenuModel() { IconFont = "\xe755", Title = "文件夹统计", BackColor = "#218868" });
- menus.Add(new MenuModel() { IconFont = "\xe635", Title = "文件编码", BackColor = "#218868" });
+ Menus =
+ [
+ new MenuModel() { IconFont = "\xe635", Title = "数据建模", BackColor = "#218868" },
+ new MenuModel() { IconFont = "\xe6b6", Title = "网络工具", BackColor = "#EE3B3B", Type = typeof(NetworkWindow) },
+ new MenuModel() { IconFont = "\xe6e1", Title = "RPC工具", BackColor = "#218868" },
+ new MenuModel() { IconFont = "\xe614", Title = "串口工具", BackColor = "#EE3B3B" },
+ new MenuModel() { IconFont = "\xe755", Title = "地图接口", BackColor = "#218868" },
+ new MenuModel() { IconFont = "\xe635", Title = "正则表达式", BackColor = "#218868", Type = typeof(RegexWindow) },
+ new MenuModel() { IconFont = "\xe6b6", Title = "图标水印", BackColor = "#EE3B3B", Type = typeof(IconToolWindow) },
+ new MenuModel() { IconFont = "\xe6e1", Title = "加密解密", BackColor = "#218868", Type = typeof(SecurityWindow) },
+ new MenuModel() { IconFont = "\xe614", Title = "语音助手", BackColor = "#EE3B3B", Type = typeof(SpeechWindow) },
+ new MenuModel() { IconFont = "\xe755", Title = "GPS 辅助", BackColor = "#218868", Type = typeof(GpsWindow) },
+ new MenuModel() { IconFont = "\xe635", Title = "MQTT 客户端", BackColor = "#EE3B3B", Type = typeof(MqttWindow) },
+ new MenuModel() { IconFont = "\xe755", Title = "文件夹统计", BackColor = "#218868", Type = typeof(FolderStatWindow) },
+ new MenuModel() { IconFont = "\xe635", Title = "文件编码", BackColor = "#218868" },
+ ];
SelectedMenu = Menus[0];
-
- SelectedCommand = new RelayCommand<MenuModel>(t => Select(t));
}
- private ObservableCollection<MenuModel> menus;
/// <summary>菜单集合</summary>
- public ObservableCollection<MenuModel> Menus
- {
- get => menus;
- set { menus = value; RaisePropertyChanged(); }
- }
-
- private MenuModel selectedMenu;
- /// <summary>选中菜单</summary>
- public MenuModel SelectedMenu
- {
- get => selectedMenu;
- set { selectedMenu = value; RaisePropertyChanged(); }
- }
+ public ObservableCollection<MenuModel> Menus { get; }
- public RelayCommand<MenuModel> SelectedCommand { get; set; }
+ [ObservableProperty]
+ private MenuModel _selectedMenu = null!;
+ [RelayCommand]
private void Select(MenuModel model)
{
SelectedMenu = model;
diff --git a/CrazyCoder/ViewModels/MqttViewModel.cs b/CrazyCoder/ViewModels/MqttViewModel.cs
new file mode 100644
index 0000000..4358616
--- /dev/null
+++ b/CrazyCoder/ViewModels/MqttViewModel.cs
@@ -0,0 +1,275 @@
+#nullable enable
+using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using NewLife;
+using NewLife.Data;
+using NewLife.Log;
+using NewLife.MQTT;
+using NewLife.MQTT.Messaging;
+using NewLife.Net;
+
+namespace CrazyCoder.ViewModels;
+
+/// <summary>MQTT 客户端 ViewModel</summary>
+public partial class MqttViewModel : ObservableObject, IDisposable
+{
+ #region 属性
+ private MqttClient? _client;
+ private readonly ILog _log;
+
+ /// <summary>服务器地址</summary>
+ [ObservableProperty]
+ private String _server = "127.0.0.1:1883";
+
+ /// <summary>客户端标识</summary>
+ [ObservableProperty]
+ private String _clientId = "";
+
+ /// <summary>用户名</summary>
+ [ObservableProperty]
+ private String _userName = "";
+
+ /// <summary>密码</summary>
+ [ObservableProperty]
+ private String _password = "";
+
+ /// <summary>是否已连接</summary>
+ [ObservableProperty]
+ private Boolean _isConnected;
+
+ /// <summary>连接按钮文本</summary>
+ [ObservableProperty]
+ private String _connectButtonText = "连接";
+
+ /// <summary>订阅主题</summary>
+ [ObservableProperty]
+ private String _subscribeTopic = "";
+
+ /// <summary>订阅 QoS</summary>
+ [ObservableProperty]
+ private Int32 _subscribeQos;
+
+ /// <summary>发布主题</summary>
+ [ObservableProperty]
+ private String _publishTopic = "";
+
+ /// <summary>发布内容</summary>
+ [ObservableProperty]
+ private String _publishText = "";
+
+ /// <summary>发布 QoS</summary>
+ [ObservableProperty]
+ private Int32 _publishQos;
+
+ /// <summary>保留消息</summary>
+ [ObservableProperty]
+ private Boolean _retain;
+
+ /// <summary>QoS 选项</summary>
+ public String[] QosOptions { get; } = ["0", "1", "2"];
+
+ /// <summary>日志文本</summary>
+ [ObservableProperty]
+ private String _logText = "";
+
+ /// <summary>日志回调</summary>
+ public Action<String>? OnLog { get; set; }
+ #endregion
+
+ #region 构造
+ /// <summary>实例化 MQTT 客户端 ViewModel</summary>
+ public MqttViewModel()
+ {
+ ClientId = Environment.MachineName;
+ SubscribeTopic = "topic/test";
+ PublishTopic = "topic/test";
+ PublishText = "Hello MQTT";
+
+ _log = new MqttLog(this);
+ }
+ #endregion
+
+ #region 连接/断开
+ /// <summary>切换连接/断开</summary>
+ [RelayCommand]
+ private async Task ToggleConnect()
+ {
+ if (IsConnected)
+ await Disconnect();
+ else
+ await Connect();
+ }
+
+ private async Task Connect()
+ {
+ _client = null;
+
+ var remote = Server;
+ var uri = new NetUri(remote);
+ if (uri.Type == NetType.Unknown) uri.Type = NetType.Tcp;
+ if (uri.Port == 0) uri.Port = 1883;
+
+ var client = new MqttClient
+ {
+ Server = $"{uri}",
+ ClientId = ClientId,
+ UserName = UserName,
+ Password = Password,
+ Log = _log,
+ };
+ client.Received += OnReceived;
+ client.Connected += (s, e) => WriteLog("连接成功");
+ client.Disconnected += (s, e) => WriteLog("连接断开");
+
+ try
+ {
+ await client.ConnectAsync();
+ _client = client;
+
+ IsConnected = true;
+ ConnectButtonText = "断开";
+
+ WriteLog("已连接服务器");
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"连接失败:{ex.Message}");
+ client.Dispose();
+ }
+ }
+
+ private async Task Disconnect()
+ {
+ if (_client != null)
+ {
+ _client.Reconnect = false;
+ try
+ {
+ await _client.DisconnectAsync();
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"断开异常:{ex.Message}");
+ }
+ _client.Dispose();
+ _client = null;
+ }
+
+ IsConnected = false;
+ ConnectButtonText = "连接";
+ WriteLog("已断开连接");
+ }
+
+ private void OnReceived(Object? sender, EventArgs<PublishMessage> e)
+ {
+ var msg = e.Arg;
+ var payload = msg.Payload?.ToStr() ?? "";
+ WriteLog($"[{msg.Topic}] {payload}");
+ }
+ #endregion
+
+ #region 订阅/发布
+ /// <summary>订阅主题</summary>
+ [RelayCommand]
+ private async Task Subscribe()
+ {
+ if (_client == null) return;
+
+ var topic = SubscribeTopic;
+ if (topic.IsNullOrEmpty())
+ {
+ MessageBox.Show("请输入订阅主题!", "提示");
+ return;
+ }
+
+ var qos = (QualityOfService)SubscribeQos;
+
+ try
+ {
+ var rs = await _client.SubscribeAsync([topic], qos);
+ WriteLog($"订阅成功,Id={rs?.Id}");
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"订阅失败:{ex.Message}");
+ }
+ }
+
+ /// <summary>发布消息</summary>
+ [RelayCommand]
+ private async Task Publish()
+ {
+ if (_client == null) return;
+
+ var topic = PublishTopic;
+ var str = PublishText;
+
+ if (topic.IsNullOrEmpty())
+ {
+ MessageBox.Show("请输入发布主题!", "提示");
+ return;
+ }
+ if (str.IsNullOrEmpty())
+ {
+ MessageBox.Show("请输入发布内容!", "提示");
+ return;
+ }
+
+ var qos = (QualityOfService)PublishQos;
+
+ try
+ {
+ WriteLog(str);
+ var rs = await _client.PublishAsync(topic, str, qos);
+ if (rs != null)
+ WriteLog($"发布成功,Id={rs.Id}");
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"发布失败:{ex.Message}");
+ }
+ }
+
+ /// <summary>清空日志</summary>
+ [RelayCommand]
+ private void ClearLog()
+ {
+ LogText = "";
+ OnLog?.Invoke("__CLEAR__");
+ }
+ #endregion
+
+ #region 日志
+ private void WriteLog(String msg)
+ {
+ OnLog?.Invoke(msg);
+ }
+
+ private class MqttLog : Logger
+ {
+ private readonly MqttViewModel _vm;
+ public MqttLog(MqttViewModel vm) => _vm = vm;
+ protected override void OnWrite(LogLevel level, String format, params Object?[] args)
+ {
+ var msg = args is { Length: > 0 } ? String.Format(format, args) : format;
+ _vm.WriteLog(msg);
+ }
+ }
+ #endregion
+
+ #region IDisposable
+ /// <summary>释放资源</summary>
+ public void Dispose()
+ {
+ if (_client != null)
+ {
+ _client.Reconnect = false;
+ try { _client.DisconnectAsync().GetAwaiter().GetResult(); } catch { }
+ _client.Dispose();
+ _client = null;
+ }
+ GC.SuppressFinalize(this);
+ }
+ #endregion
+}
diff --git a/CrazyCoder/ViewModels/NetworkViewModel.cs b/CrazyCoder/ViewModels/NetworkViewModel.cs
new file mode 100644
index 0000000..527e1a3
--- /dev/null
+++ b/CrazyCoder/ViewModels/NetworkViewModel.cs
@@ -0,0 +1,406 @@
+using System.Collections.ObjectModel;
+using System.Net;
+using System.Text;
+using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using NewLife;
+using NewLife.Data;
+using NewLife.Log;
+using NewLife.Net;
+using NewLife.Threading;
+
+namespace CrazyCoder.ViewModels;
+
+/// <summary>网络调试工具工作模式</summary>
+public enum NetworkWorkMode
+{
+ /// <summary>TCP/UDP混合</summary>
+ UdpTcp = 1,
+
+ /// <summary>UDP服务端</summary>
+ UdpServer,
+
+ /// <summary>UDP客户端</summary>
+ UdpClient,
+
+ /// <summary>TCP服务端</summary>
+ TcpServer,
+
+ /// <summary>TCP客户端</summary>
+ TcpClient
+}
+
+/// <summary>网络调试工具ViewModel</summary>
+public partial class NetworkViewModel : ObservableObject
+{
+ #region 属性
+ private NetServer _server;
+ private ISocketClient _client;
+ private TimerX _timer;
+
+ /// <summary>工作模式列表</summary>
+ public ObservableCollection<String> Modes { get; } =
+ [
+ "TCP/UDP混合",
+ "UDP服务端",
+ "UDP客户端",
+ "TCP服务端",
+ "TCP客户端"
+ ];
+
+ /// <summary>本地地址列表</summary>
+ public ObservableCollection<String> LocalAddresses { get; } = [];
+
+ [ObservableProperty]
+ private Int32 _selectedModeIndex;
+
+ [ObservableProperty]
+ private String _localAddress = "";
+
+ [ObservableProperty]
+ private String _remoteAddress = "";
+
+ [ObservableProperty]
+ private Int32 _port = 8080;
+
+ [ObservableProperty]
+ private Boolean _isConnected;
+
+ [ObservableProperty]
+ private String _connectButtonText = "打开";
+
+ [ObservableProperty]
+ private String _sendText = "新生命开发团队,学无先后达者为师";
+
+ [ObservableProperty]
+ private Boolean _hexSend;
+
+ [ObservableProperty]
+ private Int32 _sendTimes = 1;
+
+ [ObservableProperty]
+ private Int32 _sendSleep = 1000;
+
+ [ObservableProperty]
+ private Int32 _sendThreads = 1;
+
+ [ObservableProperty]
+ private Boolean _showLog = true;
+
+ [ObservableProperty]
+ private Boolean _showSocketLog = true;
+
+ [ObservableProperty]
+ private Boolean _showSend = true;
+
+ [ObservableProperty]
+ private Boolean _showReceive = true;
+
+ [ObservableProperty]
+ private Boolean _showStat = true;
+
+ [ObservableProperty]
+ private Boolean _showReceiveString = true;
+
+ [ObservableProperty]
+ private Int64 _receivedBytes;
+
+ [ObservableProperty]
+ private Int64 _sentBytes;
+
+ [ObservableProperty]
+ private Int32 _sessionCount;
+
+ /// <summary>日志回调,由View绑定到RichTextBox</summary>
+ public Action<String> OnLog { get; set; }
+ #endregion
+
+ #region 构造
+ /// <summary>实例化网络调试工具ViewModel</summary>
+ public NetworkViewModel()
+ {
+ LoadLocalAddresses();
+ }
+
+ private void LoadLocalAddresses()
+ {
+ LocalAddresses.Clear();
+ LocalAddresses.Add("所有本地IPv4/IPv6");
+ LocalAddresses.Add(IPAddress.Any.ToString());
+ LocalAddresses.Add(IPAddress.IPv6Any.ToString());
+
+ try
+ {
+ var host = System.Net.Dns.GetHostEntry("");
+ foreach (var addr in host.AddressList)
+ {
+ LocalAddresses.Add(addr.ToString());
+ }
+ }
+ catch
+ {
+ }
+ }
+ #endregion
+
+ #region 连接/断开
+ [RelayCommand]
+ private void ToggleConnect()
+ {
+ if (IsConnected)
+ {
+ Disconnect();
+ }
+ else
+ {
+ Connect();
+ }
+ }
+
+ private void Connect()
+ {
+ _server = null;
+ _client = null;
+
+ var mode = (NetworkWorkMode)(SelectedModeIndex + 1);
+ var local = LocalAddress;
+ var remote = RemoteAddress;
+ var port = Port;
+
+ switch (mode)
+ {
+ case NetworkWorkMode.UdpTcp:
+ _server = new NetServer();
+ break;
+ case NetworkWorkMode.UdpServer:
+ _server = new NetServer
+ {
+ ProtocolType = NetType.Udp
+ };
+ break;
+ case NetworkWorkMode.TcpServer:
+ _server = new NetServer
+ {
+ ProtocolType = NetType.Tcp
+ };
+ break;
+ case NetworkWorkMode.TcpClient:
+ _client = new TcpSession();
+ break;
+ case NetworkWorkMode.UdpClient:
+ _client = new UdpServer();
+ break;
+ }
+
+ if (_client != null)
+ {
+ _client.Log = ShowLog ? CreateBizLog() : Logger.Null;
+ if (!local.IsNullOrEmpty() && !local.StartsWith("所有本地"))
+ _client.Local.Host = local;
+ _client.Received += OnReceived;
+ _client.Remote.Port = port;
+ _client.Remote.Host = remote;
+
+ _client.LogSend = ShowSend;
+ _client.LogReceive = ShowReceive;
+
+ if (!_client.Open())
+ {
+ WriteLog("连接失败");
+ return;
+ }
+
+ WriteLog("已连接服务器");
+ }
+ else if (_server != null)
+ {
+ _server.Log = ShowLog ? CreateBizLog() : Logger.Null;
+ _server.SocketLog = ShowSocketLog ? CreateBizLog() : Logger.Null;
+ _server.Port = port;
+ if (!local.IsNullOrEmpty() && !local.StartsWith("所有本地"))
+ _server.Local.Host = local;
+ _server.Received += OnReceived;
+
+ _server.LogSend = ShowSend;
+ _server.LogReceive = ShowReceive;
+
+ _server.Start();
+
+ WriteLog($"正在监听 {port}");
+ }
+
+ IsConnected = true;
+ ConnectButtonText = "关闭";
+
+ _timer = new TimerX(OnShowStat, null, 5000, 5000) { Async = true };
+ }
+
+ private void Disconnect()
+ {
+ if (_client != null)
+ {
+ _client.Dispose();
+ _client = null;
+ WriteLog("关闭连接");
+ }
+
+ if (_server != null)
+ {
+ WriteLog($"停止监听 {_server.Port}");
+ _server.Dispose();
+ _server = null;
+ }
+
+ _timer?.Dispose();
+ _timer = null;
+
+ IsConnected = false;
+ ConnectButtonText = "打开";
+ }
+ #endregion
+
+ #region 收发数据
+ private String _lastStat = "";
+
+ private void OnShowStat(Object state)
+ {
+ if (!ShowStat) return;
+
+ var msg = "";
+ if (_server != null)
+ msg = _server.GetStat();
+
+ if (!msg.IsNullOrEmpty() && msg != _lastStat)
+ {
+ _lastStat = msg;
+ WriteLog(msg);
+ }
+ }
+
+ private void OnReceived(Object sender, ReceivedEventArgs e)
+ {
+ var session = sender as ISocketSession;
+ if (session == null)
+ {
+ if (sender is INetSession ns)
+ session = ns.Session;
+ }
+
+ if (session == null) return;
+
+ if (ShowReceiveString)
+ {
+ var line = e.Packet.ToStr();
+ WriteLog(line);
+ }
+
+ ReceivedBytes += e.Packet.Total;
+ SessionCount = _server?.SessionCount ?? (_client != null ? 1 : 0);
+ }
+
+ [RelayCommand]
+ private async Task Send()
+ {
+ var str = SendText;
+ if (String.IsNullOrEmpty(str))
+ {
+ MessageBox.Show("发送内容不能为空!", "网络调试");
+ return;
+ }
+
+ var count = SendTimes;
+ var sleep = SendSleep;
+ var ths = SendThreads;
+ if (count <= 0) count = 1;
+ if (sleep <= 0) sleep = 1;
+
+ // 处理换行
+ str = str.Replace("\n", "\r\n");
+ var buf = HexSend ? str.ToHex() : str.GetBytes();
+ var pk = new ArrayPacket(buf);
+
+ if (_client != null)
+ {
+ if (ths <= 1)
+ {
+ await SendHelper.SendConcurrency(_client, pk, count, sleep);
+ }
+ else
+ {
+ var any = _client.Local.Address.IsAny();
+ var list = new List<ISocketClient>();
+ for (var i = 0; i < ths; i++)
+ {
+ var client = _client.Remote.CreateRemote();
+ if (!any) client.Local.EndPoint = new IPEndPoint(_client.Local.Address, 2000 + i);
+ list.Add(client);
+ }
+
+ var tasks = list.Select(c => SendHelper.SendConcurrency(c, pk, count, sleep));
+ await Task.WhenAll(tasks);
+
+ foreach (var item in list)
+ {
+ item.TryDispose();
+ }
+ }
+
+ SentBytes += buf.Length * count;
+ }
+ else if (_server != null)
+ {
+ WriteLog($"准备向[{_server.SessionCount}]个客户端发送[{count}]次[{buf.Length}]的数据");
+ for (var i = 0; i < count && _server != null; i++)
+ {
+ var sw = System.Diagnostics.Stopwatch.StartNew();
+ var cs = await _server.SendAllAsync(pk);
+ sw.Stop();
+ WriteLog($"{i + 1}/{count} 已向[{cs}]个客户端发送[{buf.Length}]数据 {sw.ElapsedMilliseconds}ms");
+ if (sleep > 0) await Task.Delay(sleep);
+ }
+
+ SentBytes += buf.Length * count * (_server?.SessionCount ?? 0);
+ }
+ }
+
+ [RelayCommand]
+ private void ClearReceiveLog()
+ {
+ WriteLog("__CLEAR__");
+ ReceivedBytes = 0;
+ }
+ #endregion
+
+ #region 日志
+ private ILog CreateBizLog()
+ {
+ return TextFileLog.Create(null, "Net_{0:yyyy_MM_dd}.log");
+ }
+
+ /// <summary>写入日志到UI</summary>
+ /// <param name="msg">日志消息</param>
+ public void WriteLog(String msg)
+ {
+ OnLog?.Invoke(msg);
+ }
+ #endregion
+}
+
+/// <summary>异步多次发送数据</summary>
+internal static class SendHelper
+{
+ public static async Task SendConcurrency(ISocketRemote session, IPacket pk, Int32 times, Int32 msInterval)
+ {
+ await Task.Run(async () =>
+ {
+ for (var i = 0; i < times; i++)
+ {
+ session.Send(pk);
+
+ if (msInterval > 0)
+ await Task.Delay(msInterval);
+ }
+ });
+ }
+}
diff --git a/CrazyCoder/ViewModels/RegexViewModel.cs b/CrazyCoder/ViewModels/RegexViewModel.cs
new file mode 100644
index 0000000..50d7dd0
--- /dev/null
+++ b/CrazyCoder/ViewModels/RegexViewModel.cs
@@ -0,0 +1,473 @@
+#nullable enable
+
+using System;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CrazyCoder.Models;
+using Microsoft.Win32;
+
+namespace CrazyCoder.ViewModels
+{
+ /// <summary>正则表达式工具 ViewModel</summary>
+ public partial class RegexViewModel : ObservableObject
+ {
+ #region 属性
+
+ /// <summary>正则表达式</summary>
+ [ObservableProperty]
+ private String _pattern = "";
+
+ /// <summary>源文本</summary>
+ [ObservableProperty]
+ private String _sourceText = "";
+
+ /// <summary>替换文本</summary>
+ [ObservableProperty]
+ private String _replacement = "";
+
+ /// <summary>忽略大小写</summary>
+ [ObservableProperty]
+ private Boolean _ignoreCase;
+
+ /// <summary>多行模式</summary>
+ [ObservableProperty]
+ private Boolean _multiline;
+
+ /// <summary>单行模式</summary>
+ [ObservableProperty]
+ private Boolean _singleline;
+
+ /// <summary>忽略空白</summary>
+ [ObservableProperty]
+ private Boolean _ignorePatternWhitespace;
+
+ /// <summary>匹配模式(false 为替换模式)</summary>
+ [ObservableProperty]
+ private Boolean _isMatchMode = true;
+
+ /// <summary>当前选项文本</summary>
+ [ObservableProperty]
+ private String _optionText = "RegexOptions.None";
+
+ /// <summary>状态文本</summary>
+ [ObservableProperty]
+ private String _status = "就绪";
+
+ /// <summary>目录路径</summary>
+ [ObservableProperty]
+ private String _directoryPath = "";
+
+ /// <summary>文件过滤</summary>
+ [ObservableProperty]
+ private String _fileFilter = "*.*";
+
+ /// <summary>选中的匹配项</summary>
+ [ObservableProperty]
+ private RegexMatchItem? _selectedMatch;
+
+ /// <summary>选中的分组</summary>
+ [ObservableProperty]
+ private RegexGroupItem? _selectedGroup;
+
+ /// <summary>选中的捕获</summary>
+ [ObservableProperty]
+ private RegexCaptureItem? _selectedCapture;
+
+ /// <summary>匹配结果集合</summary>
+ public ObservableCollection<RegexMatchItem> Matches { get; } = [];
+
+ /// <summary>分组结果集合</summary>
+ public ObservableCollection<RegexGroupItem> Groups { get; } = [];
+
+ /// <summary>捕获结果集合</summary>
+ public ObservableCollection<RegexCaptureItem> Captures { get; } = [];
+
+ /// <summary>执行按钮文本</summary>
+ public String ExecuteButtonText => IsMatchMode ? "正则匹配" : "正则替换";
+
+ /// <summary>替换模式</summary>
+ public Boolean IsReplaceMode => !IsMatchMode;
+
+ /// <summary>替换面板可见性</summary>
+ public Visibility ReplacePanelVisibility => IsMatchMode ? Visibility.Collapsed : Visibility.Visible;
+
+ /// <summary>目录浏览面板可见性</summary>
+ public Visibility BatchPanelVisibility => IsMatchMode ? Visibility.Collapsed : Visibility.Visible;
+
+ /// <summary>结果面板可见性</summary>
+ public Visibility ResultPanelVisibility => IsMatchMode ? Visibility.Visible : Visibility.Collapsed;
+
+ #endregion
+
+ #region 构造
+
+ /// <summary>当前正则对象(用于分组名称查询)</summary>
+ private Regex? _regex;
+
+ #endregion
+
+ #region 选项变更
+
+ partial void OnIgnoreCaseChanged(Boolean value) => UpdateOptionText();
+ partial void OnMultilineChanged(Boolean value) => UpdateOptionText();
+ partial void OnSinglelineChanged(Boolean value) => UpdateOptionText();
+ partial void OnIgnorePatternWhitespaceChanged(Boolean value) => UpdateOptionText();
+
+ partial void OnIsMatchModeChanged(Boolean value)
+ {
+ OnPropertyChanged(nameof(ExecuteButtonText));
+ OnPropertyChanged(nameof(IsReplaceMode));
+ OnPropertyChanged(nameof(ReplacePanelVisibility));
+ OnPropertyChanged(nameof(BatchPanelVisibility));
+ OnPropertyChanged(nameof(ResultPanelVisibility));
+ }
+
+ #endregion
+
+ #region 选中项变更
+
+ partial void OnSelectedMatchChanged(RegexMatchItem? value)
+ {
+ Groups.Clear();
+ Captures.Clear();
+
+ if (value?.Match == null) return;
+
+ var reg = _regex;
+ var m = value.Match;
+ for (var i = 0; i < m.Groups.Count; i++)
+ {
+ var g = m.Groups[i];
+ var name = reg?.GroupNameFromNumber(i) ?? i.ToString();
+ Groups.Add(new RegexGroupItem
+ {
+ Index = i,
+ Name = name,
+ Value = g.Value,
+ Position = g.Index,
+ Length = g.Length,
+ Group = g
+ });
+ }
+ }
+
+ partial void OnSelectedGroupChanged(RegexGroupItem? value)
+ {
+ Captures.Clear();
+
+ if (value?.Group == null) return;
+
+ var g = value.Group;
+ for (var i = 0; i < g.Captures.Count; i++)
+ {
+ var c = g.Captures[i];
+ Captures.Add(new RegexCaptureItem
+ {
+ Index = i,
+ Value = c.Value,
+ Position = c.Index,
+ Length = c.Length,
+ Capture = c
+ });
+ }
+ }
+
+ #endregion
+
+ #region 辅助方法
+
+ /// <summary>更新选项文本</summary>
+ void UpdateOptionText()
+ {
+ var sb = new StringBuilder();
+ var options = GetOptions();
+ if (options == RegexOptions.None)
+ {
+ sb.Append("RegexOptions.None");
+ }
+ else
+ {
+ if ((options & RegexOptions.IgnoreCase) != 0) AppendOption(sb, "IgnoreCase");
+ if ((options & RegexOptions.Multiline) != 0) AppendOption(sb, "Multiline");
+ if ((options & RegexOptions.Singleline) != 0) AppendOption(sb, "Singleline");
+ if ((options & RegexOptions.IgnorePatternWhitespace) != 0) AppendOption(sb, "IgnorePatternWhitespace");
+ }
+ OptionText = sb.ToString();
+ }
+
+ /// <summary>追加选项名称</summary>
+ static void AppendOption(StringBuilder sb, String name)
+ {
+ if (sb.Length > 0) sb.Append(" | ");
+ sb.Append("RegexOptions.");
+ sb.Append(name);
+ }
+
+ /// <summary>获取当前 RegexOptions</summary>
+ RegexOptions GetOptions()
+ {
+ var options = RegexOptions.None;
+ if (IgnoreCase) options |= RegexOptions.IgnoreCase;
+ if (Multiline) options |= RegexOptions.Multiline;
+ if (Singleline) options |= RegexOptions.Singleline;
+ if (IgnorePatternWhitespace) options |= RegexOptions.IgnorePatternWhitespace;
+ return options;
+ }
+
+ /// <summary>计算行号</summary>
+ static Int32 GetLineFromIndex(String text, Int32 index)
+ {
+ if (String.IsNullOrEmpty(text) || index <= 0) return 1;
+ var line = 1;
+ for (var i = 0; i < index && i < text.Length; i++)
+ {
+ if (text[i] == '\n') line++;
+ }
+ return line;
+ }
+
+ #endregion
+
+ #region 命令
+
+ /// <summary>执行匹配或替换</summary>
+ [RelayCommand]
+ async Task ExecuteAsync()
+ {
+ if (String.IsNullOrEmpty(Pattern)) return;
+ var text = SourceText;
+ if (String.IsNullOrEmpty(text)) return;
+
+ if (!IsMatchMode)
+ {
+ await ReplaceTextAsync(text);
+ return;
+ }
+
+ Status = "正在匹配...";
+ Matches.Clear();
+ Groups.Clear();
+ Captures.Clear();
+
+ var pattern = Pattern;
+ var options = GetOptions();
+ var timeout = TimeSpan.FromSeconds(5);
+
+ await Task.Run(() =>
+ {
+ try
+ {
+ var sw = Stopwatch.StartNew();
+ var reg = new Regex(pattern, options, timeout);
+ _regex = reg;
+
+ var ms = reg.Matches(text);
+ var idx = 1;
+ foreach (Match match in ms)
+ {
+ var line = GetLineFromIndex(text, match.Index);
+ var item = new RegexMatchItem
+ {
+ Index = idx++,
+ Value = match.Value,
+ Line = line,
+ Position = match.Index,
+ Length = match.Length,
+ Match = match
+ };
+
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ Matches.Add(item);
+ });
+ }
+
+ sw.Stop();
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ Status = $"成功匹配 {Matches.Count} 项!耗时 {sw.ElapsedMilliseconds}ms";
+ });
+ }
+ catch (RegexMatchTimeoutException)
+ {
+ Application.Current.Dispatcher.Invoke(() => Status = "正则匹配超时!");
+ }
+ catch (Exception ex)
+ {
+ Application.Current.Dispatcher.Invoke(() => Status = $"错误:{ex.Message}");
+ }
+ });
+ }
+
+ /// <summary>执行替换</summary>
+ async Task ReplaceTextAsync(String text)
+ {
+ var replacement = Replacement;
+ if (String.IsNullOrEmpty(replacement))
+ {
+ Status = "请输入替换文本";
+ return;
+ }
+
+ Status = "正在替换...";
+ var pattern = Pattern;
+ var options = GetOptions();
+ var timeout = TimeSpan.FromSeconds(5);
+
+ await Task.Run(() =>
+ {
+ try
+ {
+ var sw = Stopwatch.StartNew();
+ var reg = new Regex(pattern, options, timeout);
+ _regex = reg;
+
+ var count = 0;
+ var ms = reg.Matches(text);
+ if (ms != null) count = ms.Count;
+
+ if (count > 0)
+ {
+ var result = reg.Replace(text, replacement);
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ SourceText = result;
+ });
+ }
+
+ sw.Stop();
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ Status = $"成功替换 {count} 项!耗时 {sw.ElapsedMilliseconds}ms";
+ });
+ }
+ catch (RegexMatchTimeoutException)
+ {
+ Application.Current.Dispatcher.Invoke(() => Status = "正则替换超时!");
+ }
+ catch (Exception ex)
+ {
+ Application.Current.Dispatcher.Invoke(() => Status = $"错误:{ex.Message}");
+ }
+ });
+ }
+
+ /// <summary>浏览目录</summary>
+ [RelayCommand]
+ void BrowseDirectory()
+ {
+ var dialog = new OpenFolderDialog
+ {
+ Title = "选择目录"
+ };
+
+ if (dialog.ShowDialog() == true)
+ {
+ DirectoryPath = dialog.FolderName;
+ }
+ }
+
+ /// <summary>批量替换</summary>
+ [RelayCommand]
+ async Task BatchReplaceAsync()
+ {
+ if (String.IsNullOrEmpty(Pattern))
+ {
+ Status = "请输入正则表达式";
+ return;
+ }
+ var replacement = Replacement;
+ if (String.IsNullOrEmpty(replacement))
+ {
+ Status = "请输入替换文本";
+ return;
+ }
+
+ var path = DirectoryPath;
+ if (String.IsNullOrEmpty(path) || !Directory.Exists(path))
+ {
+ Status = "请先选择有效目录";
+ return;
+ }
+
+ var filter = FileFilter;
+ if (String.IsNullOrEmpty(filter)) filter = "*.*";
+
+ Status = "正在批量替换...";
+ var pattern = Pattern;
+ var options = GetOptions();
+
+ await Task.Run(() =>
+ {
+ try
+ {
+ var sw = Stopwatch.StartNew();
+ var reg = new Regex(pattern, options, TimeSpan.FromSeconds(5));
+
+ var files = Directory.GetFiles(path, filter, SearchOption.AllDirectories);
+ if (files == null || files.Length == 0)
+ {
+ Application.Current.Dispatcher.Invoke(() => Status = "没有符合条件的文件");
+ return;
+ }
+
+ var totalCount = 0;
+ var changedCount = 0;
+ String? firstContent = null;
+
+ foreach (var item in files.Take(1000))
+ {
+ try
+ {
+ var content = File.ReadAllText(item);
+ var ms = reg.Matches(content);
+ if (ms == null || ms.Count == 0) continue;
+
+ totalCount += ms.Count;
+ var content2 = reg.Replace(content, replacement);
+ if (content != content2)
+ {
+ File.WriteAllText(item, content2);
+ changedCount++;
+ if (firstContent == null) firstContent = content2;
+ }
+ }
+ catch
+ {
+ // 跳过无法读取的文件
+ }
+ }
+
+ sw.Stop();
+
+ if (firstContent != null)
+ {
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ SourceText = firstContent;
+ });
+ }
+
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ Status = $"批量替换完成:{changedCount} 个文件,{totalCount} 项替换,耗时 {sw.ElapsedMilliseconds}ms";
+ });
+ }
+ catch (Exception ex)
+ {
+ Application.Current.Dispatcher.Invoke(() => Status = $"错误:{ex.Message}");
+ }
+ });
+ }
+
+ #endregion
+ }
+}
diff --git a/CrazyCoder/ViewModels/SecurityViewModel.cs b/CrazyCoder/ViewModels/SecurityViewModel.cs
new file mode 100644
index 0000000..7a805f5
--- /dev/null
+++ b/CrazyCoder/ViewModels/SecurityViewModel.cs
@@ -0,0 +1,804 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Management;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Security.Cryptography;
+using System.Text;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Microsoft.Win32;
+using NewLife;
+using NewLife.Collections;
+using NewLife.Data;
+using NewLife.Reflection;
+using NewLife.Security;
+using NewLife.Serialization;
+using NewLife.Web;
+
+namespace CrazyCoder.ViewModels;
+
+/// <summary>加密解密工具 ViewModel</summary>
+public partial class SecurityViewModel : ObservableObject
+{
+ #region 属性
+ /// <summary>原文</summary>
+ [ObservableProperty]
+ private String _sourceText = "学无先后达者为师";
+
+ /// <summary>密码</summary>
+ [ObservableProperty]
+ private String _passText = "NewLife";
+
+ /// <summary>结果</summary>
+ [ObservableProperty]
+ private String _resultText = "";
+
+ // 原文格式单选
+ /// <summary>原文格式-字符串</summary>
+ [ObservableProperty]
+ private Boolean _isSourceString = true;
+
+ /// <summary>原文格式-HEX</summary>
+ [ObservableProperty]
+ private Boolean _isSourceHex;
+
+ /// <summary>原文格式-Base64</summary>
+ [ObservableProperty]
+ private Boolean _isSourceBase64;
+
+ /// <summary>HEX单选可用</summary>
+ [ObservableProperty]
+ private Boolean _sourceHexEnabled = true;
+
+ /// <summary>Base64单选可用</summary>
+ [ObservableProperty]
+ private Boolean _sourceBase64Enabled = true;
+
+ // 密码格式单选
+ /// <summary>密码格式-字符串</summary>
+ [ObservableProperty]
+ private Boolean _isPassString = true;
+
+ /// <summary>密码格式-HEX</summary>
+ [ObservableProperty]
+ private Boolean _isPassHex;
+
+ /// <summary>密码格式-Base64</summary>
+ [ObservableProperty]
+ private Boolean _isPassBase64;
+
+ // 结果格式多选
+ /// <summary>结果格式-字符串</summary>
+ [ObservableProperty]
+ private Boolean _isResultString = true;
+
+ /// <summary>结果格式-HEX</summary>
+ [ObservableProperty]
+ private Boolean _isResultHex = true;
+
+ /// <summary>结果格式-Base64</summary>
+ [ObservableProperty]
+ private Boolean _isResultBase64 = true;
+ #endregion
+
+ #region 源文本变更 - 检测HEX/Base64可用性
+ partial void OnSourceTextChanged(String value)
+ {
+ if (value.IsNullOrEmpty()) return;
+
+ // 非纯ASCII(多字节字符)时禁用HEX/Base64单选
+ var enc = Encoding.UTF8;
+ if (enc.GetByteCount(value) != value.Length)
+ {
+ SourceHexEnabled = false;
+ SourceBase64Enabled = false;
+ return;
+ }
+
+ // 检测是否为合法HEX
+ try
+ {
+ SourceHexEnabled = value.ToHex().Length > 0;
+ }
+ catch
+ {
+ SourceHexEnabled = false;
+ }
+
+ // 检测是否为合法Base64
+ try
+ {
+ SourceBase64Enabled = value.ToBase64().Length > 0;
+ }
+ catch
+ {
+ SourceBase64Enabled = false;
+ }
+ }
+ #endregion
+
+ #region 辅助方法
+ /// <summary>从字符串中智能获取字节数组。自动识别 HEX(含-) → Base64 → UTF8</summary>
+ /// <param name="str">字符串</param>
+ /// <returns>字节数组</returns>
+ private static Byte[] GetBytes(String str)
+ {
+ if (str.IsNullOrEmpty()) return [];
+
+ try
+ {
+ if (str.Contains('-')) return str.ToHex();
+ }
+ catch { }
+
+ try
+ {
+ return str.ToBase64();
+ }
+ catch { }
+
+ return str.GetBytes();
+ }
+
+ /// <summary>按单选按钮状态从原文取字节</summary>
+ /// <returns>字节数组</returns>
+ private Byte[] GetSource()
+ {
+ var v = SourceText;
+
+ if (IsSourceString) return v.GetBytes();
+ if (IsSourceHex) return v.ToHex();
+ if (IsSourceBase64) return v.ToBase64();
+
+ return null;
+ }
+
+ /// <summary>按单选按钮状态从密码区取字节</summary>
+ /// <returns>字节数组</returns>
+ private Byte[] GetPass()
+ {
+ var v = PassText;
+
+ if (IsPassString) return v.GetBytes();
+ if (IsPassHex) return v.ToHex();
+ if (IsPassBase64) return v.ToBase64();
+
+ return null;
+ }
+
+ /// <summary>多行输出结果</summary>
+ /// <param name="rs">字符串数组</param>
+ private void SetResult(params String[] rs)
+ {
+ var sb = new StringBuilder();
+ foreach (var item in rs)
+ {
+ if (sb.Length > 0) sb.AppendLine();
+ sb.Append(item);
+ }
+ ResultText = sb.ToString();
+ }
+
+ /// <summary>按输出格式复选框输出字节数组</summary>
+ /// <param name="data">字节数组</param>
+ /// <returns>输出字符串列表</returns>
+ private List<String> SetResult(Byte[] data)
+ {
+ var list = new List<String>();
+ if (IsResultString) list.Add(data.ToStr());
+ if (IsResultHex)
+ {
+ list.Add(data.ToHex().ToUpper());
+ list.Add(data.ToHex().ToLower());
+ list.Add(data.ToHex("-"));
+ list.Add(data.ToHex(" "));
+ }
+ if (IsResultBase64)
+ {
+ list.Add(data.ToBase64());
+ list.Add(data.ToUrlBase64());
+ }
+
+ SetResult(list.ToArray());
+
+ return list;
+ }
+ #endregion
+
+ #region 功能命令
+ /// <summary>上下互换:交换原文区和结果区内容</summary>
+ [RelayCommand]
+ private void Exchange()
+ {
+ var v = SourceText;
+ var v2 = ResultText;
+ // 结果区只要第一行
+ if (!v2.IsNullOrEmpty())
+ {
+ var ss = v2.Split('\n');
+ var n = 0;
+ if (ss.Length > n + 1 && ss[n].StartsWith("/*") && ss[n].EndsWith("*/")) n++;
+ v2 = ss[n];
+ }
+ SourceText = v2;
+ ResultText = v;
+ }
+
+ /// <summary>HEX编码</summary>
+ [RelayCommand]
+ private void HexEncode()
+ {
+ var buf = GetSource();
+ SetResult(buf.ToHex(), buf.ToHex(" ", 32), buf.ToHex("-", 32));
+ }
+
+ /// <summary>HEX解码</summary>
+ [RelayCommand]
+ private void HexDecode()
+ {
+ var v = SourceText;
+ ResultText = v?.Trim().ToHex().ToStr();
+ }
+
+ /// <summary>Base64编码</summary>
+ [RelayCommand]
+ private void Base64Encode()
+ {
+ var buf = GetSource();
+ SetResult(buf.ToBase64(), buf.ToUrlBase64());
+ }
+
+ /// <summary>Base64解码</summary>
+ [RelayCommand]
+ private void Base64Decode()
+ {
+ var v = SourceText;
+
+ var vs = v.Split('.');
+ if (vs.Length <= 1)
+ {
+ var buf = v.Trim().ToBase64();
+ SetResult(buf);
+ }
+ else
+ {
+ SetResult(vs.Select(e2 => e2.Trim().ToBase64().ToStr()).ToArray());
+ }
+ }
+
+ /// <summary>MD5_32位</summary>
+ [RelayCommand]
+ private void MD5_32()
+ {
+ var buf = GetSource();
+ buf = buf.MD5();
+ SetResult(buf);
+ }
+
+ /// <summary>MD5_16位</summary>
+ [RelayCommand]
+ private void MD5_16()
+ {
+ var buf = GetSource();
+ buf = buf.MD5().Take(8).ToArray();
+ SetResult(buf);
+ }
+
+ /// <summary>SHA1</summary>
+ [RelayCommand]
+ private void SHA1()
+ {
+ var buf = GetSource();
+ var key = GetPass();
+
+ buf = buf.SHA1(key);
+ var rs = SetResult(buf);
+ rs.Add($"sha1${key.ToStr()}${buf.ToBase64()}");
+
+ SetResult(rs.ToArray());
+ }
+
+ /// <summary>SHA256</summary>
+ [RelayCommand]
+ private void SHA256()
+ {
+ var buf = GetSource();
+ var key = GetPass();
+
+ buf = buf.SHA256(key);
+ var rs = SetResult(buf);
+ rs.Add($"sha256${key.ToStr()}${buf.ToBase64()}");
+
+ SetResult(rs.ToArray());
+ }
+
+ /// <summary>SHA384</summary>
+ [RelayCommand]
+ private void SHA384()
+ {
+ var buf = GetSource();
+ var key = GetPass();
+
+ buf = buf.SHA384(key);
+ var rs = SetResult(buf);
+ rs.Add($"sha384${key.ToStr()}${buf.ToBase64()}");
+
+ SetResult(rs.ToArray());
+ }
+
+ /// <summary>SHA512</summary>
+ [RelayCommand]
+ private void SHA512()
+ {
+ var buf = GetSource();
+ var key = GetPass();
+
+ buf = buf.SHA512(key);
+ var rs = SetResult(buf);
+ rs.Add($"sha512${key.ToStr()}${buf.ToBase64()}");
+
+ SetResult(rs.ToArray());
+ }
+
+ /// <summary>CRC_32</summary>
+ [RelayCommand]
+ private void CRC_32()
+ {
+ var buf = GetSource();
+ var rs = buf.Crc();
+ var data = rs.GetBytes(false);
+ SetResult("/*数字、HEX编码、Base64编码*/", rs + "", data.ToHex(), data.ToBase64());
+ }
+
+ /// <summary>CRC_16</summary>
+ [RelayCommand]
+ private void CRC_16()
+ {
+ var buf = GetSource();
+ var rs = buf.Crc16();
+ var data = rs.GetBytes(false);
+ var mcrc = Modbus_CRC(buf, 0, buf.Length);
+ SetResult("/*数字、HEX编码、Base64编码、Modbus-Crc*/", rs + "", data.ToHex(), data.ToBase64(), mcrc.GetBytes().ToHex("-"));
+ }
+
+ /// <summary>RSA加密</summary>
+ [RelayCommand]
+ private void RSAEncrypt()
+ {
+ var buf = GetSource();
+ var key = PassText;
+
+ if (key.Length < 100)
+ {
+ key = RSAHelper.GenerateKey().First();
+ PassText = key;
+ }
+
+ buf = RSAHelper.Encrypt(buf, key);
+
+ SetResult(buf);
+ }
+
+ /// <summary>RSA解密</summary>
+ [RelayCommand]
+ private void RSADecrypt()
+ {
+ var buf = GetSource();
+ var pass = PassText;
+
+ try
+ {
+ buf = RSAHelper.Decrypt(buf, pass, true);
+ }
+ catch (CryptographicException)
+ {
+ // 换一种填充方式
+ buf = RSAHelper.Decrypt(buf, pass, false);
+ }
+
+ SetResult(buf);
+ }
+
+ /// <summary>DSA签名</summary>
+ [RelayCommand]
+ private void DSASign()
+ {
+ var buf = GetSource();
+ var key = PassText;
+
+ if (key.Length < 100)
+ {
+ key = DSAHelper.GenerateKey().First();
+ PassText = key;
+ }
+
+ buf = DSAHelper.Sign(buf, key);
+
+ SetResult(buf);
+ }
+
+ /// <summary>DSA验证</summary>
+ [RelayCommand]
+ private void DSAVerify()
+ {
+ var buf = GetSource();
+ var pass = PassText;
+
+ var v = ResultText;
+ if (v.Contains("\n\n")) v = v.Substring(null, "\n\n");
+ var sign = GetBytes(v);
+
+ var rs = DSAHelper.Verify(buf, pass, sign);
+ SetResult(rs ? "验证通过" : "验证失败");
+ }
+
+ /// <summary>Url编码</summary>
+ [RelayCommand]
+ private void UrlEncode()
+ {
+ var v = SourceText;
+ v = WebUtility.UrlEncode(v);
+ ResultText = v;
+ }
+
+ /// <summary>Url解码</summary>
+ [RelayCommand]
+ private void UrlDecode()
+ {
+ var v = SourceText;
+ v = WebUtility.UrlDecode(v);
+ ResultText = v;
+ }
+
+ /// <summary>Html编码</summary>
+ [RelayCommand]
+ private void HtmlEncode()
+ {
+ var v = SourceText;
+ v = WebUtility.HtmlEncode(v);
+ ResultText = v;
+ }
+
+ /// <summary>Html解码</summary>
+ [RelayCommand]
+ private void HtmlDecode()
+ {
+ var v = SourceText;
+ v = WebUtility.HtmlDecode(v);
+ ResultText = v;
+ }
+
+ /// <summary>时间戳</summary>
+ [RelayCommand]
+ private void Timestamp()
+ {
+ var v = SourceText;
+ if (v.IsNullOrEmpty()) return;
+
+ var sb = Pool.StringBuilder.Get();
+
+ DateTime.TryParse(PassText, out var baseTime);
+ var utc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
+ if (baseTime > DateTime.MinValue) sb.AppendFormat("基准:{0:yyyy-MM-dd HH:mm:ss.fff} {1}\r\n", baseTime, baseTime.Kind);
+
+ var dt = v.ToDateTime();
+ if (dt.Year > 1 && dt.Year < 3000)
+ {
+ var s = dt.ToInt();
+ var m = dt.ToLong();
+
+ if (baseTime > DateTime.MinValue)
+ {
+ s -= baseTime.ToInt();
+ m -= baseTime.ToLong();
+ }
+
+ sb.AppendLine("Unix秒:" + s);
+ sb.AppendLine("Unix毫秒:" + m);
+ }
+
+ var now = DateTime.Now;
+ var n = v.ToLong();
+ if (n >= Int32.MaxValue)
+ {
+ dt = n.ToDateTime();
+ dt = dt.ToLocalTime().ToUniversalTime();
+ if (baseTime > DateTime.MinValue) dt = dt.Add(baseTime - utc);
+ if (dt.Year > 1000 && dt.Year < 3000)
+ {
+ sb.AppendFormat("时间:{0:yyyy-MM-dd HH:mm:ss.fff} (Unix毫秒) {1}\r\n", dt, dt.Kind);
+ dt = dt.ToLocalTime();
+ sb.AppendFormat("时间:{0:yyyy-MM-dd HH:mm:ss.fff} (现在) {1}\r\n", dt, dt.Kind);
+ }
+ }
+ else if (n > 0)
+ {
+ dt = v.ToInt().ToDateTime();
+ dt = dt.ToLocalTime().ToUniversalTime();
+ if (baseTime > DateTime.MinValue) dt = dt.Add(baseTime - utc);
+ if (dt.Year > 1000 && dt.Year < 3000)
+ {
+ sb.AppendFormat("时间:{0:yyyy-MM-dd HH:mm:ss} (Unix秒) {1}\r\n", dt, dt.Kind);
+ dt = dt.ToLocalTime();
+ sb.AppendFormat("时间:{0:yyyy-MM-dd HH:mm:ss} (现在) {1}\r\n", dt, dt.Kind);
+ }
+ }
+
+ // 过去/未来时间戳
+ if (n > 0 && n < 1000L * 365 * 24 * 3600 * 1000)
+ {
+ sb.AppendFormat("过去:{0:yyyy-MM-dd HH:mm:ss.fff} (now.AddMilliseconds(-n))\r\n", now.AddMilliseconds(-n));
+ sb.AppendFormat("未来:{0:yyyy-MM-dd HH:mm:ss.fff} (now.AddMilliseconds(n))\r\n", now.AddMilliseconds(n));
+
+ if (n < Int32.MaxValue)
+ {
+ sb.AppendFormat("过去:{0:yyyy-MM-dd HH:mm:ss} (now.AddSeconds(-n))\r\n", now.AddSeconds(-n));
+ sb.AppendFormat("未来:{0:yyyy-MM-dd HH:mm:ss} (now.AddSeconds(n))\r\n", now.AddSeconds(n));
+ }
+ }
+
+ ResultText = sb.Return(true);
+ }
+
+ /// <summary>机器信息</summary>
+ [RelayCommand]
+ private void ComputerInfo()
+ {
+ var sb = Pool.StringBuilder.Get();
+
+ var mi = MachineInfo.Current;
+ mi.Refresh();
+ sb.AppendLine(mi.ToJson(true));
+ sb.AppendLine();
+
+ var osName = GetWmiInfo("Win32_OperatingSystem", "Caption");
+ sb.AppendFormat("OSName:\t{0}\t(Win32_OperatingSystem.Caption)\r\n", osName);
+
+ var osVersion = GetWmiInfo("Win32_OperatingSystem", "Version");
+ sb.AppendFormat("OSVersion:\t{0}\t(Win32_OperatingSystem.Version)\r\n", osVersion);
+
+ var macs = GetMacs().ToList();
+ if (macs.Count > 0) sb.AppendFormat("MAC:\t{0}\r\n", macs.Join(",", x => x.ToHex("-")));
+
+ var processor = GetWmiInfo("Win32_Processor", "Name");
+ sb.AppendFormat("Processor:\t{0}\t(Win32_Processor.Name)\r\n", processor);
+
+ var cpuID = GetWmiInfo("Win32_Processor", "ProcessorId");
+ sb.AppendFormat("ProcessorId:\t{0}\t(Win32_Processor.ProcessorId)\r\n", cpuID);
+
+ var uuid = GetWmiInfo("Win32_ComputerSystemProduct", "UUID");
+ sb.AppendFormat("UUID:\t{0}\t(Win32_ComputerSystemProduct.UUID)\r\n", uuid);
+
+ var id = GetWmiInfo("Win32_ComputerSystemProduct", "IdentifyingNumber");
+ sb.AppendFormat("IdentifyingNumber:\t{0}\t(Win32_ComputerSystemProduct.IdentifyingNumber)\r\n", id);
+
+ var bios = GetWmiInfo("Win32_BIOS", "SerialNumber");
+ sb.AppendFormat("BIOS:\t{0}\t(Win32_BIOS.SerialNumber)\r\n", bios);
+
+ var baseBoard = GetWmiInfo("Win32_BaseBoard", "SerialNumber");
+ sb.AppendFormat("BaseBoard:\t{0}\t(Win32_BaseBoard.SerialNumber)\r\n", baseBoard);
+
+ var serialNumber = GetWmiInfo("Win32_DiskDrive", "SerialNumber");
+ sb.AppendFormat("DiskSerial:\t{0}\t(Win32_DiskDrive.SerialNumber)\r\n", serialNumber);
+
+ var reg = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
+ if (reg != null)
+ {
+ var guid = reg.GetValue("MachineGuid") + "";
+ sb.AppendFormat("MachineGuid:\t{0}\t(SOFTWARE\\Microsoft\\Cryptography)\r\n", guid);
+ }
+
+ ResultText = sb.Return(true);
+ }
+
+ private static readonly String[] _Excludes = ["Loopback", "VMware", "VBox", "Virtual", "Teredo", "Microsoft", "VPN", "VNIC", "IEEE"];
+
+ /// <summary>获取所有网卡MAC地址</summary>
+ /// <returns>MAC地址集合</returns>
+ public static IEnumerable<Byte[]> GetMacs()
+ {
+ foreach (var item in NetworkInterface.GetAllNetworkInterfaces())
+ {
+ if (_Excludes.Any(e => item.Description.Contains(e))) continue;
+ if (item.Speed < 1_000_000) continue;
+
+ var addrs = item.GetIPProperties().UnicastAddresses.Where(e => e.Address.AddressFamily == AddressFamily.InterNetwork).ToArray();
+ if (addrs.All(e => IPAddress.IsLoopback(e.Address))) continue;
+
+ var mac = item.GetPhysicalAddress()?.GetAddressBytes();
+ if (mac != null && mac.Length == 6) yield return mac;
+ }
+ }
+
+ /// <summary>获取WMI信息</summary>
+ /// <param name="path">WMI路径</param>
+ /// <param name="property">属性名</param>
+ /// <returns>属性值</returns>
+ public static String GetWmiInfo(String path, String property)
+ {
+ try
+ {
+ var wql = $"Select {property} From {path}";
+ var searcher = new ManagementObjectSearcher(wql);
+ var bbs = new List<String>();
+ foreach (var mo in searcher.Get())
+ {
+ if (mo != null && mo.Properties != null && mo.Properties[property] != null && mo.Properties[property].Value != null)
+ bbs.Add(mo.Properties[property].Value.ToString());
+ }
+ bbs.Sort();
+ return bbs.Join(",");
+ }
+ catch
+ {
+ return "";
+ }
+ }
+
+ /// <summary>雪花Id</summary>
+ [RelayCommand]
+ private void Snowflake()
+ {
+ var v = SourceText.ToLong();
+ if (v <= 0) return;
+
+ var snow = new Snowflake();
+
+ // 指定基准时间
+ if (!PassText.IsNullOrEmpty())
+ {
+ var baseTime = PassText.ToDateTime();
+ if (baseTime.Year > 1000) snow.StartTimestamp = baseTime;
+ }
+
+ if (!snow.TryParse(v, out var time, out var workerId, out var sequence)) throw new Exception("解码失败!");
+
+ // 初始化一次,用于获取本机workerId
+ snow.NewId();
+
+ var t = (Int64)(time - snow.StartTimestamp).TotalMilliseconds;
+ SetResult(
+ $"十六:{v:X16}",
+ $"编码:{(t << 2):X8} {workerId:X2} {sequence:X3}",
+ $"基准:{snow.StartTimestamp.ToFullString()} {snow.StartTimestamp.Kind}",
+ $"时间:{time:yyyy-MM-dd HH:mm:ss.fff} {time.Kind} ({t} / {(t << 22):X8})",
+ $"节点:{workerId} ({workerId:X4})",
+ $"序号:{sequence} ({sequence:X4})",
+ $"本机:{snow.WorkerId} ({snow.WorkerId:X4})");
+ }
+
+ /// <summary>JWT令牌</summary>
+ [RelayCommand]
+ private void JwtToken()
+ {
+ var v = SourceText;
+ if (v.IsNullOrEmpty()) return;
+
+ var pass = PassText?.Trim();
+
+ var vs = v.Split('.');
+ if (vs.Length == 3)
+ {
+ var jwt = new JwtBuilder { Secret = "abcd" };
+
+ var ss = pass?.Split(':');
+ if (ss != null && ss.Length >= 2)
+ {
+ jwt.Algorithm = ss[0];
+ jwt.Secret = ss[1];
+ }
+
+ var rs = jwt.TryDecode(v, out var message);
+
+ SetResult($"验证结果:{rs}", jwt.ToJson(true));
+ }
+ else if (vs.Length == 2)
+ {
+ var prv = new TokenProvider
+ {
+ Key = pass
+ };
+
+ var rs = prv.TryDecode(v, out var user, out var expire);
+
+ SetResult($"验证结果:{rs}", new { user, expire }.ToJson(true));
+ }
+ else
+ {
+ SetResult(vs.Select(e2 => e2.ToBase64().ToStr()).ToArray());
+ }
+ }
+
+ /// <summary>版本号</summary>
+ [RelayCommand]
+ private void Version()
+ {
+ var v = SourceText;
+ if (v.IsNullOrEmpty()) return;
+
+ var dt = AssemblyX.GetCompileTime(v);
+
+ ResultText = dt.ToFullString();
+ }
+
+ /// <summary>TraceId</summary>
+ [RelayCommand]
+ private void TraceId()
+ {
+ var str = SourceText.Trim();
+ if (str.IsNullOrEmpty()) return;
+
+ var rs = new List<String>();
+ foreach (var item in str.Split('\n'))
+ {
+ var v = item.Trim();
+ if (v.IsNullOrEmpty()) continue;
+
+ // TraceId
+ if (v.Length >= 30)
+ {
+ if (v.Length >= 8)
+ rs.Add($"地址:{v[..8]} {new IPAddress(v[..8].ToHex())}");
+
+ if (v.Length >= 8 + 13)
+ {
+ var time = v.Substring(8, 13).ToLong();
+ rs.Add($"时间:{time}({time.ToDateTime().ToLocalTime().ToFullString()})");
+ }
+
+ if (v.Length >= 21 + 4)
+ rs.Add($"序列:{v[21..25]} {v[21..25].ToHex().ToUInt16()}");
+
+ if (v.Length >= 26 + 4)
+ rs.Add($"进程:{v[26..30]} {v[26..30].ToHex().ToUInt16()}");
+ }
+ else if (v.Length >= 16)
+ {
+ if (v.Length >= 8)
+ rs.Add($"地址:{v[..8]} {new IPAddress(v[..8].ToHex())}");
+
+ if (v.Length >= 8 + 4)
+ rs.Add($"进程:{v[8..12]} {v[8..12].ToHex().ToUInt16()}");
+
+ if (v.Length >= 12 + 4)
+ rs.Add($"序列:{v[12..16]} {v[12..16].ToHex().ToUInt16()}");
+ }
+ rs.Add("");
+ }
+
+ SetResult(rs.ToArray());
+ }
+ #endregion
+
+ #region Modbus_CRC
+ private static readonly UInt16[] crc_ta = [0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400];
+
+ /// <summary>Crc校验</summary>
+ /// <param name="data">数据</param>
+ /// <param name="offset">偏移</param>
+ /// <param name="count">数量</param>
+ /// <returns>Crc结果</returns>
+ public static UInt16 Modbus_CRC(Byte[] data, Int32 offset, Int32 count = -1)
+ {
+ if (data == null || data.Length < 1) return 0;
+
+ UInt16 u = 0xFFFF;
+ Byte b;
+
+ if (count == 0) count = data.Length - offset;
+
+ for (var i = offset; i < count; i++)
+ {
+ b = data[i];
+ u = (UInt16)(crc_ta[(b ^ u) & 15] ^ (u >> 4));
+ u = (UInt16)(crc_ta[((b >> 4) ^ u) & 15] ^ (u >> 4));
+ }
+
+ return u;
+ }
+ #endregion
+}
diff --git a/CrazyCoder/ViewModels/SpeechViewModel.cs b/CrazyCoder/ViewModels/SpeechViewModel.cs
new file mode 100644
index 0000000..9413897
--- /dev/null
+++ b/CrazyCoder/ViewModels/SpeechViewModel.cs
@@ -0,0 +1,178 @@
+#nullable enable
+using System.Collections.ObjectModel;
+using System.Speech.Synthesis;
+using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Microsoft.Win32;
+using NewLife;
+
+namespace CrazyCoder.ViewModels;
+
+/// <summary>语音助手 (TTS) ViewModel</summary>
+public partial class SpeechViewModel : ObservableObject
+{
+ #region 属性
+ private SpeechSynthesizer? _synth;
+
+ /// <summary>可用语音列表</summary>
+ public ObservableCollection<String> Voices { get; } = [];
+
+ /// <summary>语音数据列表</summary>
+ private List<VoiceInfo> _voiceInfos = [];
+
+ /// <summary>选中语音索引</summary>
+ [ObservableProperty]
+ private Int32 _selectedVoiceIndex;
+
+ /// <summary>音量 (0-100)</summary>
+ [ObservableProperty]
+ private Int32 _volume = 100;
+
+ /// <summary>语速 (-10 到 10)</summary>
+ [ObservableProperty]
+ private Int32 _rate;
+
+ /// <summary>朗读文本</summary>
+ [ObservableProperty]
+ private String _speakText = "学无先后达者为师";
+
+ /// <summary>是否正在朗读</summary>
+ [ObservableProperty]
+ private Boolean _isSpeaking;
+ #endregion
+
+ #region 构造
+ /// <summary>实例化语音助手 ViewModel</summary>
+ public SpeechViewModel()
+ {
+ LoadVoices();
+ }
+
+ private void LoadVoices()
+ {
+ try
+ {
+ using var sync = new SpeechSynthesizer();
+ var voices = sync.GetInstalledVoices();
+ _voiceInfos = voices.Select(e => e.VoiceInfo).ToList();
+
+ Voices.Clear();
+ foreach (var vi in _voiceInfos)
+ {
+ Voices.Add($"{vi.Name}[{vi.Culture}]");
+ }
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"加载语音失败:{ex.Message}", "提示");
+ }
+ }
+ #endregion
+
+ #region 命令
+ /// <summary>朗读</summary>
+ [RelayCommand]
+ private void Speak()
+ {
+ var txt = SpeakText;
+ if (txt.IsNullOrEmpty()) return;
+
+ Stop();
+
+ try
+ {
+ _synth = CreateSynthesizer();
+ _synth.SpeakCompleted += OnSpeakCompleted;
+ IsSpeaking = true;
+ _synth.SpeakAsync(txt);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"朗读失败:{ex.Message}", "提示");
+ IsSpeaking = false;
+ }
+ }
+
+ /// <summary>停止朗读</summary>
+ [RelayCommand]
+ private void Stop()
+ {
+ if (_synth != null)
+ {
+ try
+ {
+ _synth.SpeakAsyncCancelAll();
+ }
+ catch
+ {
+ }
+ _synth.Dispose();
+ _synth = null;
+ }
+ IsSpeaking = false;
+ }
+
+ /// <summary>保存为 WAV 文件</summary>
+ [RelayCommand]
+ private void SaveWav()
+ {
+ var txt = SpeakText;
+ if (txt.IsNullOrEmpty())
+ {
+ MessageBox.Show("请输入要转换的文本!", "提示");
+ return;
+ }
+
+ var dlg = new SaveFileDialog
+ {
+ Filter = "音频文件 (*.wav)|*.wav",
+ Title = "保存为 WAV 文件",
+ DefaultExt = "wav"
+ };
+ if (dlg.ShowDialog() != true) return;
+
+ try
+ {
+ using var sync = CreateSynthesizer();
+ sync.SetOutputToWaveFile(dlg.FileName);
+ sync.Speak(txt);
+
+ MessageBox.Show("保存成功!", "提示");
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"保存失败:{ex.Message}", "提示");
+ }
+ }
+ #endregion
+
+ #region 辅助
+ private SpeechSynthesizer CreateSynthesizer()
+ {
+ var sync = new SpeechSynthesizer
+ {
+ Volume = Volume,
+ Rate = Rate
+ };
+
+ if (SelectedVoiceIndex >= 0 && SelectedVoiceIndex < _voiceInfos.Count)
+ {
+ sync.SelectVoice(_voiceInfos[SelectedVoiceIndex].Name);
+ }
+
+ return sync;
+ }
+
+ private void OnSpeakCompleted(Object? sender, SpeakCompletedEventArgs e)
+ {
+ IsSpeaking = false;
+ if (_synth != null)
+ {
+ _synth.SpeakCompleted -= OnSpeakCompleted;
+ _synth.Dispose();
+ _synth = null;
+ }
+ }
+ #endregion
+}
diff --git a/CrazyCoder/Views/FolderStatWindow.xaml b/CrazyCoder/Views/FolderStatWindow.xaml
new file mode 100644
index 0000000..dda4bbb
--- /dev/null
+++ b/CrazyCoder/Views/FolderStatWindow.xaml
@@ -0,0 +1,42 @@
+<Window x:Class="CrazyCoder.Views.FolderStatWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d"
+ Title="文件夹大小统计" Height="700" Width="1000" WindowStartupLocation="CenterScreen">
+ <Window.Resources>
+ <ContextMenu x:Key="FolderContextMenu">
+ <MenuItem Header="打开目录" Click="OnOpenDirectory"/>
+ <MenuItem Header="删除" Click="OnDelete"/>
+ </ContextMenu>
+ </Window.Resources>
+
+ <Grid Margin="4">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="4"/>
+ <RowDefinition Height="150"/>
+ </Grid.RowDefinitions>
+
+ <!-- 树形目录 -->
+ <TreeView x:Name="treeView" BorderThickness="1" BorderBrush="#D0D0D0">
+ <TreeView.ItemContainerStyle>
+ <Style TargetType="TreeViewItem">
+ <Setter Property="ContextMenu" Value="{StaticResource FolderContextMenu}"/>
+ <EventSetter Event="TreeViewItem.Expanded" Handler="OnItemExpanded"/>
+ <EventSetter Event="TreeViewItem.Collapsed" Handler="OnItemCollapsed"/>
+ <EventSetter Event="TreeViewItem.PreviewMouseRightButtonDown" Handler="OnItemPreviewMouseRightButtonDown"/>
+ </Style>
+ </TreeView.ItemContainerStyle>
+ </TreeView>
+
+ <GridSplitter Grid.Row="1" Height="4" HorizontalAlignment="Stretch" Background="#F0F0F0"/>
+
+ <!-- 日志输出 -->
+ <Border Grid.Row="2" BorderBrush="#D0D0D0" BorderThickness="1">
+ <RichTextBox x:Name="txtLog" IsReadOnly="True" VerticalScrollBarVisibility="Auto"
+ FontFamily="Consolas" FontSize="12" Background="#1E1E1E" Foreground="#D4D4D4"/>
+ </Border>
+ </Grid>
+</Window>
diff --git a/CrazyCoder/Views/FolderStatWindow.xaml.cs b/CrazyCoder/Views/FolderStatWindow.xaml.cs
new file mode 100644
index 0000000..56f3695
--- /dev/null
+++ b/CrazyCoder/Views/FolderStatWindow.xaml.cs
@@ -0,0 +1,308 @@
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using NewLife.Caching;
+
+namespace CrazyCoder.Views;
+
+/// <summary>文件夹大小统计工具</summary>
+public partial class FolderStatWindow : Window
+{
+ private const String DummyNode = "\0";
+
+ private readonly ICache _cache = new MemoryCache();
+
+ #region 初始化
+ /// <summary>实例化文件夹大小统计窗口</summary>
+ public FolderStatWindow()
+ {
+ InitializeComponent();
+
+ // 加载固定硬盘驱动器作为根节点
+ foreach (var item in DriveInfo.GetDrives())
+ {
+ if (item.DriveType == DriveType.Fixed)
+ {
+ var header = $"{item.Name,-10} ({FormatSize(item.TotalSize)})";
+ var tn = new TreeViewItem
+ {
+ Header = header,
+ Tag = item.RootDirectory
+ };
+ // 预加虚拟子节点,展开时触发 MakeTree
+ tn.Items.Add(new TreeViewItem { Header = DummyNode });
+ treeView.Items.Add(tn);
+ }
+ }
+ }
+ #endregion
+
+ #region 构造目录树
+ private void MakeTree(TreeViewItem parent)
+ {
+ if (parent.Tag is not DirectoryInfo di) return;
+
+ WriteLog($"展开目录 {di.FullName}");
+
+ parent.Items.Clear();
+
+ var list = new List<FileSystemInfo>();
+ list.AddRange(di.GetDirectories());
+ list.AddRange(di.GetFiles());
+
+ var max = 0;
+ foreach (var item in list)
+ {
+ max = Math.Max(max, StrLen(item.Name));
+ }
+ max++;
+
+ foreach (var item in list)
+ {
+ var len = max;
+ len -= (StrLen(item.Name) - item.Name.Length);
+ Int64 size;
+
+ if (item is FileInfo fi)
+ size = fi.Length;
+ else
+ size = -1;
+
+ var header = String.Format("{0,-" + len + "} {1,10}", item.Name, FormatSize(size));
+ var tn = new TreeViewItem
+ {
+ Header = header,
+ Tag = item,
+ Background = GetBrush(size)
+ };
+
+ if (item is DirectoryInfo)
+ {
+ // 预加虚拟子节点,展开时继续加载
+ tn.Items.Add(new TreeViewItem { Header = DummyNode });
+
+ // 后台线程异步统计文件夹大小
+ var captured = tn;
+ Task.Run(() => TongJi(captured));
+ }
+
+ parent.Items.Add(tn);
+ }
+ }
+
+ /// <summary>格式化文件大小</summary>
+ private static String FormatSize(Int64 size)
+ {
+ if (size < 1024) return size + " Byte";
+ var ds = (Double)size / 1024;
+ if (ds < 1024) return ds.ToString("N2") + " K";
+ ds /= 1024;
+ if (ds < 1024) return ds.ToString("N2") + " M";
+ ds /= 1024;
+ if (ds < 1024) return ds.ToString("N2") + " G";
+ ds /= 1024;
+ if (ds < 1024) return ds.ToString("N2") + " T";
+ return "∞";
+ }
+
+ /// <summary>计算字符串显示宽度(中文字符算2个宽度)</summary>
+ private static Int32 StrLen(String str) => (str.Length + Encoding.UTF8.GetByteCount(str)) / 2;
+ #endregion
+
+ #region 统计文件夹大小
+ private Int64 FolderSize(DirectoryInfo di)
+ {
+ if (_cache.TryGetValue<Int64>(di.FullName, out var v)) return v;
+
+ Int64 size = 0;
+ try
+ {
+ foreach (var item in di.GetFiles())
+ {
+ size += item.Length;
+ }
+ foreach (var item in di.GetDirectories())
+ {
+ size += FolderSize(item);
+ }
+ }
+ catch { }
+
+ // 缓存30秒,避免重复遍历
+ _cache.Set(di.FullName, size, 30);
+
+ return size;
+ }
+ #endregion
+
+ #region 统计目录并设置大小
+ private void TongJi(TreeViewItem node)
+ {
+ if (node.Tag is not DirectoryInfo di) return;
+
+ try
+ {
+ var size = FolderSize(di);
+ var header = node.Header as String;
+ if (header != null && header.Length >= 10)
+ {
+ var str = header[..^10] + $"{FormatSize(size),10}";
+ Dispatcher.Invoke(() =>
+ {
+ node.Header = str;
+ node.Background = GetBrush(size);
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteLog(ex.ToString());
+ }
+ }
+
+ /// <summary>根据大小获取背景色</summary>
+ private static Brush GetBrush(Int64 size)
+ {
+ if (size > 1024L * 1024 * 1024) return Brushes.Yellow;
+ if (size > 100L * 1024 * 1024) return Brushes.LawnGreen;
+ if (size > 1024L * 1024) return Brushes.LightBlue;
+ if (size > 1024L) return Brushes.MistyRose;
+ return Brushes.White;
+ }
+ #endregion
+
+ #region 展开折叠目录树
+ private void OnItemExpanded(Object sender, RoutedEventArgs e)
+ {
+ if (sender is TreeViewItem item && item.Tag is DirectoryInfo)
+ {
+ // 只有虚拟子节点时触发懒加载
+ if (item.Items.Count == 1 && item.Items[0] is TreeViewItem dummy && DummyNode.Equals(dummy.Header))
+ {
+ MakeTree(item);
+ }
+ }
+ }
+
+ private void OnItemCollapsed(Object sender, RoutedEventArgs e)
+ {
+ if (sender is TreeViewItem item && item.Tag is DirectoryInfo)
+ {
+ // 折叠后清空,再次展开时重新计算
+ item.Items.Clear();
+ item.Items.Add(new TreeViewItem { Header = DummyNode });
+ }
+ }
+ #endregion
+
+ #region 右键菜单
+ private static String GetSelectedPath(TreeView tree)
+ {
+ if (tree.SelectedItem is TreeViewItem node && node.Tag is FileSystemInfo fi)
+ return fi.FullName;
+
+ return null;
+ }
+
+ private void OnItemPreviewMouseRightButtonDown(Object sender, MouseButtonEventArgs e)
+ {
+ if (sender is TreeViewItem item)
+ {
+ item.Focus();
+ item.IsSelected = true;
+ }
+ }
+
+ private void OnOpenDirectory(Object sender, RoutedEventArgs e)
+ {
+ var path = GetSelectedPath(treeView);
+ if (String.IsNullOrEmpty(path)) return;
+
+ try
+ {
+ if (File.Exists(path))
+ Process.Start("explorer.exe", "/select,\"" + path + "\"");
+ else if (Directory.Exists(path))
+ Process.Start("explorer.exe", "\"" + path + "\"");
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(ex.Message, "打开出错");
+ }
+ }
+
+ private void OnDelete(Object sender, RoutedEventArgs e)
+ {
+ var path = GetSelectedPath(treeView);
+ if (String.IsNullOrEmpty(path)) return;
+
+ if (MessageBox.Show($"准备删除{path}\n删除将不可恢复,是否删除?", "确认删除", MessageBoxButton.YesNo) != MessageBoxResult.Yes)
+ return;
+
+ try
+ {
+ if (File.Exists(path))
+ File.Delete(path);
+ else if (Directory.Exists(path))
+ DeleteRecursive(new DirectoryInfo(path));
+
+ if (treeView.SelectedItem is TreeViewItem node)
+ {
+ RemoveNode(node);
+ }
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(ex.Message, "删除出错");
+ }
+ }
+
+ private void RemoveNode(TreeViewItem node)
+ {
+ // 从父级容器移除(父级可能是 TreeView 或 TreeViewItem)
+ if (node.Parent is ItemsControl parentControl)
+ {
+ parentControl.Items.Remove(node);
+ }
+ }
+
+ /// <summary>递归删除目录</summary>
+ private void DeleteRecursive(DirectoryInfo di)
+ {
+ foreach (var item in di.GetFiles())
+ {
+ WriteLog($"删除 {item.FullName}");
+ try { item.Delete(); }
+ catch (Exception ex) { WriteLog(ex.Message); }
+ }
+ foreach (var item in di.GetDirectories())
+ {
+ DeleteRecursive(item);
+ }
+ try { di.Delete(true); }
+ catch (Exception ex) { WriteLog(ex.Message); }
+ }
+ #endregion
+
+ #region 日志
+ /// <summary>输出日志到 RichTextBox</summary>
+ private void WriteLog(String message)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ var paragraph = new Paragraph(new Run(message))
+ {
+ Foreground = Brushes.White,
+ Margin = new Thickness(0)
+ };
+ txtLog.Document.Blocks.Add(paragraph);
+ txtLog.ScrollToEnd();
+ });
+ }
+ #endregion
+}
diff --git a/CrazyCoder/Views/GpsWindow.xaml b/CrazyCoder/Views/GpsWindow.xaml
new file mode 100644
index 0000000..5c6f7f3
--- /dev/null
+++ b/CrazyCoder/Views/GpsWindow.xaml
@@ -0,0 +1,154 @@
+<Window x:Class="CrazyCoder.Views.GpsWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d"
+ Title="GPS 辅助工具" Height="450" Width="600" WindowStartupLocation="CenterScreen">
+ <Window.Resources>
+ <Style x:Key="GroupBorder" TargetType="Border">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,6"/>
+ </Style>
+
+ <Style x:Key="SectionTitle" TargetType="TextBlock">
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Setter Property="Margin" Value="4,3"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+
+ <Style x:Key="ConfigLabel" TargetType="TextBlock">
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ <Setter Property="Margin" Value="4,2"/>
+ </Style>
+
+ <Style x:Key="ConfigTextBox" TargetType="TextBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="FontFamily" Value="Consolas"/>
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ </Style>
+
+ <Style x:Key="ActionButton" TargetType="Button">
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Padding" Value="12,0"/>
+ </Style>
+
+ <Style x:Key="ResultText" TargetType="TextBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="FontFamily" Value="Consolas"/>
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Setter Property="IsReadOnly" Value="True"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="Background" Value="#FFFFF0"/>
+ </Style>
+ </Window.Resources>
+
+ <Grid Margin="8">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <!-- 合并输入 -->
+ <Border Grid.Row="0" Style="{StaticResource GroupBorder}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="HEX 合并输入(16位)" Style="{StaticResource SectionTitle}"/>
+ <Grid Grid.Row="1" Margin="4,0,4,4">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <TextBox Grid.Column="0" Text="{Binding HexCombined, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+ <Button Grid.Column="1" Content="转换" Command="{Binding ConvertCombinedCommand}"
+ Style="{StaticResource ActionButton}" Margin="4,2" Width="80"/>
+ </Grid>
+ </Grid>
+ </Border>
+
+ <!-- 分离输入 -->
+ <Border Grid.Row="1" Style="{StaticResource GroupBorder}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="HEX 分离输入(各8位)" Style="{StaticResource SectionTitle}"/>
+ <Grid Grid.Row="1" Margin="4,0,4,4">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Column="0" Text="纬度" Style="{StaticResource ConfigLabel}" Width="40"/>
+ <TextBox Grid.Column="1" Text="{Binding HexLat, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+ <TextBlock Grid.Column="2" Text="经度" Style="{StaticResource ConfigLabel}" Width="40" Margin="8,2,2,2"/>
+ <TextBox Grid.Column="3" Text="{Binding HexLong, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+ <Button Grid.Column="4" Content="转换" Command="{Binding ConvertCommand}"
+ Style="{StaticResource ActionButton}" Margin="4,2" Width="80"/>
+ </Grid>
+ </Grid>
+ </Border>
+
+ <!-- 结果输出 -->
+ <Border Grid.Row="2" Style="{StaticResource GroupBorder}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="转换结果" Style="{StaticResource SectionTitle}"/>
+
+ <Grid Grid.Row="1" Margin="4,0,4,2">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Column="0" Text="纬度" Style="{StaticResource ConfigLabel}" Width="40"/>
+ <TextBox Grid.Column="1" Text="{Binding Latitude}" Style="{StaticResource ResultText}"/>
+
+ <TextBlock Grid.Column="2" Text="经度" Style="{StaticResource ConfigLabel}" Width="40" Margin="8,2,2,2"/>
+ <TextBox Grid.Column="3" Text="{Binding Longitude}" Style="{StaticResource ResultText}"/>
+ </Grid>
+
+ <Grid Grid.Row="2" Margin="4,0,4,4">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Column="0" Text="坐标" Style="{StaticResource ConfigLabel}" Width="40"/>
+ <TextBox Grid.Column="1" Text="{Binding LatLong}" Style="{StaticResource ResultText}"/>
+ <Button Grid.Column="2" Content="清空" Command="{Binding ClearCommand}"
+ Style="{StaticResource ActionButton}" Margin="4,2" Width="60"/>
+ </Grid>
+ </Grid>
+ </Border>
+ </Grid>
+</Window>
diff --git a/CrazyCoder/Views/GpsWindow.xaml.cs b/CrazyCoder/Views/GpsWindow.xaml.cs
new file mode 100644
index 0000000..e89ff62
--- /dev/null
+++ b/CrazyCoder/Views/GpsWindow.xaml.cs
@@ -0,0 +1,16 @@
+using System.Windows;
+using CrazyCoder.ViewModels;
+
+namespace CrazyCoder.Views;
+
+/// <summary>GPS 辅助工具窗口</summary>
+public partial class GpsWindow : Window
+{
+ /// <summary>实例化 GPS 辅助工具窗口</summary>
+ public GpsWindow()
+ {
+ InitializeComponent();
+
+ DataContext = new GpsViewModel();
+ }
+}
diff --git a/CrazyCoder/Views/IconToolWindow.xaml b/CrazyCoder/Views/IconToolWindow.xaml
new file mode 100644
index 0000000..2c8d415
--- /dev/null
+++ b/CrazyCoder/Views/IconToolWindow.xaml
@@ -0,0 +1,173 @@
+<Window x:Class="CrazyCoder.Views.IconToolWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d"
+ Title="图标水印处理工具" Height="750" Width="1100" WindowStartupLocation="CenterScreen">
+ <Window.Resources>
+ <Style x:Key="GroupBorder" TargetType="Border">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,6"/>
+ </Style>
+
+ <Style x:Key="SectionTitle" TargetType="TextBlock">
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Setter Property="Margin" Value="4,3"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+
+ <Style x:Key="ConfigLabel" TargetType="TextBlock">
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ <Setter Property="Margin" Value="4,2"/>
+ </Style>
+
+ <Style x:Key="ConfigTextBox" TargetType="TextBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ </Style>
+
+ <Style x:Key="SmallNumeric" TargetType="TextBox">
+ <Setter Property="Width" Value="70"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="HorizontalContentAlignment" Value="Right"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="Margin" Value="2"/>
+ </Style>
+
+ <Style x:Key="ActionButton" TargetType="Button">
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Padding" Value="8,0"/>
+ </Style>
+ </Window.Resources>
+
+ <Grid Margin="6">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="260"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- ============ 左侧:配置面板 ============ -->
+ <ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" Margin="0,0,4,0">
+ <StackPanel>
+ <!-- 图片加载 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="图片加载" Style="{StaticResource SectionTitle}"/>
+ <Button Content="选择图片" Command="{Binding LoadImageCommand}"
+ Style="{StaticResource ActionButton}" Margin="4,2"/>
+ <TextBlock Text="提示:支持拖放图片到右侧预览区" Margin="4,0,4,4"
+ Foreground="Gray" FontSize="11"/>
+ </StackPanel>
+ </Border>
+
+ <!-- 水印设置 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="水印设置" Style="{StaticResource SectionTitle}"/>
+
+ <TextBlock Text="文字内容" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Text="{Binding WatermarkText, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <Grid Margin="4,2">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="字体" Style="{StaticResource ConfigLabel}" Width="40"/>
+ <ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding FontNames}"
+ Text="{Binding FontName, UpdateSourceTrigger=PropertyChanged}"
+ Height="26" Margin="2" IsEditable="True"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="字号" Style="{StaticResource ConfigLabel}" Width="40"/>
+ <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding FontSize, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource SmallNumeric}"/>
+ </Grid>
+
+ <Grid Margin="4,2">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="X" Style="{StaticResource ConfigLabel}" Width="20"/>
+ <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding WatermarkX, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource SmallNumeric}" Width="Auto"/>
+ <TextBlock Grid.Row="0" Grid.Column="2" Text="Y" Style="{StaticResource ConfigLabel}" Width="20" Margin="4,2,2,2"/>
+ <TextBox Grid.Row="0" Grid.Column="3" Text="{Binding WatermarkY, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource SmallNumeric}" Width="Auto"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="颜色" Style="{StaticResource ConfigLabel}" Width="40"/>
+ <TextBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3"
+ Text="{Binding WatermarkColor, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+ </Grid>
+
+ <Button Content="应用水印" Command="{Binding MakeWaterCommand}"
+ Style="{StaticResource ActionButton}" Margin="4,2"/>
+ </StackPanel>
+ </Border>
+
+ <!-- ICO 图标生成 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="ICO 图标生成" Style="{StaticResource SectionTitle}"/>
+
+ <ItemsControl ItemsSource="{Binding IconSizes}" Margin="4,2">
+ <ItemsControl.ItemTemplate>
+ <DataTemplate>
+ <CheckBox Content="{Binding Name}" IsChecked="{Binding IsSelected}" Margin="2"/>
+ </DataTemplate>
+ </ItemsControl.ItemTemplate>
+ </ItemsControl>
+
+ <Button Content="生成 ICO 图标" Command="{Binding MakeIcoCommand}"
+ Style="{StaticResource ActionButton}" Margin="4,2" FontWeight="Bold"/>
+ <Button Content="保存图片" Command="{Binding SaveImageCommand}"
+ Style="{StaticResource ActionButton}" Margin="4,2"/>
+ </StackPanel>
+ </Border>
+ </StackPanel>
+ </ScrollViewer>
+
+ <!-- ============ 右侧:图片预览 ============ -->
+ <Border Grid.Column="1" BorderBrush="#D0D0D0" BorderThickness="1" AllowDrop="True"
+ x:Name="previewBorder">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <TextBlock Text="图片预览" Style="{StaticResource SectionTitle}"/>
+
+ <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
+ <Image x:Name="previewImage" Source="{Binding PreviewImage}"
+ Stretch="None" Margin="4"
+ AllowDrop="True"
+ Drop="OnImageDrop"
+ DragOver="OnImageDragOver"/>
+ </ScrollViewer>
+ </Grid>
+ </Border>
+ </Grid>
+</Window>
diff --git a/CrazyCoder/Views/IconToolWindow.xaml.cs b/CrazyCoder/Views/IconToolWindow.xaml.cs
new file mode 100644
index 0000000..9f4c45e
--- /dev/null
+++ b/CrazyCoder/Views/IconToolWindow.xaml.cs
@@ -0,0 +1,42 @@
+using System.Windows;
+using CrazyCoder.ViewModels;
+
+namespace CrazyCoder.Views;
+
+/// <summary>图标水印处理工具窗口</summary>
+public partial class IconToolWindow : Window
+{
+ /// <summary>ViewModel</summary>
+ public IconToolViewModel ViewModel { get; }
+
+ /// <summary>实例化图标水印处理工具窗口</summary>
+ public IconToolWindow()
+ {
+ InitializeComponent();
+
+ ViewModel = new IconToolViewModel();
+ DataContext = ViewModel;
+ }
+
+ private void OnImageDrop(Object sender, DragEventArgs e)
+ {
+ if (e.Data.GetDataPresent(DataFormats.FileDrop))
+ {
+ var files = (String[])e.Data.GetData(DataFormats.FileDrop);
+ if (files is { Length: > 0 })
+ {
+ ViewModel.LoadFromFile(files[0]);
+ }
+ }
+ }
+
+ private void OnImageDragOver(Object sender, DragEventArgs e)
+ {
+ if (e.Data.GetDataPresent(DataFormats.FileDrop))
+ e.Effects = DragDropEffects.Copy;
+ else
+ e.Effects = DragDropEffects.None;
+
+ e.Handled = true;
+ }
+}
diff --git a/CrazyCoder/Views/MqttWindow.xaml b/CrazyCoder/Views/MqttWindow.xaml
new file mode 100644
index 0000000..4d6da4d
--- /dev/null
+++ b/CrazyCoder/Views/MqttWindow.xaml
@@ -0,0 +1,235 @@
+<Window x:Class="CrazyCoder.Views.MqttWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d"
+ Title="MQTT 客户端" Height="750" Width="1100" WindowStartupLocation="CenterScreen">
+ <Window.Resources>
+ <Style x:Key="GroupBorder" TargetType="Border">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,6"/>
+ </Style>
+
+ <Style x:Key="SectionTitle" TargetType="TextBlock">
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Setter Property="Margin" Value="4,3"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+
+ <Style x:Key="ConfigLabel" TargetType="TextBlock">
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ <Setter Property="Margin" Value="4,2"/>
+ </Style>
+
+ <Style x:Key="ConfigTextBox" TargetType="TextBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ </Style>
+
+ <Style x:Key="ActionButton" TargetType="Button">
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Padding" Value="8,0"/>
+ </Style>
+
+ <Style x:Key="SendButton" TargetType="Button">
+ <Setter Property="Height" Value="50"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="FontWeight" Value="Bold"/>
+ </Style>
+ </Window.Resources>
+
+ <Grid Margin="6">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="300"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- ============ 左侧:配置面板 ============ -->
+ <ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" Margin="0,0,4,0">
+ <StackPanel>
+ <!-- 连接配置 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="连接配置" Style="{StaticResource SectionTitle}"/>
+
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="50"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="地址" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Server, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="标识" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding ClientId, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="用户" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <TextBlock Grid.Row="3" Grid.Column="0" Text="密码" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <Button Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2"
+ Content="{Binding ConnectButtonText}"
+ Command="{Binding ToggleConnectCommand}"
+ Style="{StaticResource ActionButton}" FontWeight="Bold" Margin="2,4"/>
+ </Grid>
+ </StackPanel>
+ </Border>
+
+ <!-- 订阅配置 -->
+ <Border Style="{StaticResource GroupBorder}" IsEnabled="{Binding IsConnected}">
+ <StackPanel>
+ <TextBlock Text="订阅配置" Style="{StaticResource SectionTitle}"/>
+
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="50"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="主题" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding SubscribeTopic, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="2" Text="QoS" Style="{StaticResource ConfigLabel}" Margin="4,2"/>
+ <ComboBox Grid.Row="0" Grid.Column="3" ItemsSource="{Binding QosOptions}"
+ SelectedIndex="{Binding SubscribeQos}"
+ Width="50" Height="26" Margin="2"/>
+
+ <Button Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3"
+ Content="订阅" Command="{Binding SubscribeCommand}"
+ Style="{StaticResource ActionButton}" Margin="2,4"/>
+ </Grid>
+ </StackPanel>
+ </Border>
+
+ <!-- 发布配置 -->
+ <Border Style="{StaticResource GroupBorder}" IsEnabled="{Binding IsConnected}">
+ <StackPanel>
+ <TextBlock Text="发布配置" Style="{StaticResource SectionTitle}"/>
+
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="50"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="主题" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding PublishTopic, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource ConfigTextBox}"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="2" Text="QoS" Style="{StaticResource ConfigLabel}" Margin="4,2"/>
+ <ComboBox Grid.Row="0" Grid.Column="3" ItemsSource="{Binding QosOptions}"
+ SelectedIndex="{Binding PublishQos}"
+ Width="50" Height="26" Margin="2"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="保留" Style="{StaticResource ConfigLabel}"/>
+ <CheckBox Grid.Row="1" Grid.Column="1" IsChecked="{Binding Retain}" Margin="4,2"/>
+ </Grid>
+ </StackPanel>
+ </Border>
+ </StackPanel>
+ </ScrollViewer>
+
+ <!-- ============ 右侧:日志 + 发送 ============ -->
+ <Grid Grid.Column="1">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="3*"/>
+ <RowDefinition Height="4"/>
+ <RowDefinition Height="2*"/>
+ </Grid.RowDefinitions>
+
+ <!-- 收发日志 -->
+ <Border Grid.Row="0" Style="{StaticResource GroupBorder}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <TextBlock Grid.Column="0" Text="收发日志" Style="{StaticResource SectionTitle}"/>
+ <Button Grid.Column="1" Content="清空" Command="{Binding ClearLogCommand}"
+ Style="{StaticResource ActionButton}" Margin="2"/>
+ </Grid>
+
+ <RichTextBox x:Name="txtLog" Grid.Row="1"
+ IsReadOnly="True" VerticalScrollBarVisibility="Auto"
+ FontFamily="Consolas" FontSize="12"
+ Background="#1E1E1E" Foreground="#D4D4D4"
+ BorderThickness="0" Margin="2"/>
+ </Grid>
+ </Border>
+
+ <GridSplitter Grid.Row="1" Height="4" HorizontalAlignment="Stretch" Background="#F0F0F0"/>
+
+ <!-- 发送区 -->
+ <Border Grid.Row="2" Style="{StaticResource GroupBorder}" IsEnabled="{Binding IsConnected}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <TextBlock Text="发送区" Style="{StaticResource SectionTitle}"/>
+
+ <Grid Grid.Row="1" Margin="2">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="120"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBox Grid.Column="0"
+ Text="{Binding PublishText, UpdateSourceTrigger=PropertyChanged}"
+ AcceptsReturn="True" AcceptsTab="True"
+ VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
+ FontFamily="Consolas" FontSize="13"
+ BorderThickness="1" BorderBrush="#D0D0D0"
+ Margin="0,0,4,0"/>
+
+ <Button Grid.Column="1" Content="发送"
+ Command="{Binding PublishCommand}"
+ Style="{StaticResource SendButton}"
+ VerticalAlignment="Bottom"/>
+ </Grid>
+ </Grid>
+ </Border>
+ </Grid>
+ </Grid>
+</Window>
diff --git a/CrazyCoder/Views/MqttWindow.xaml.cs b/CrazyCoder/Views/MqttWindow.xaml.cs
new file mode 100644
index 0000000..6ee1dec
--- /dev/null
+++ b/CrazyCoder/Views/MqttWindow.xaml.cs
@@ -0,0 +1,49 @@
+using System.Windows;
+using System.Windows.Documents;
+using System.Windows.Media;
+using CrazyCoder.ViewModels;
+
+namespace CrazyCoder.Views;
+
+/// <summary>MQTT 客户端窗口</summary>
+public partial class MqttWindow : Window
+{
+ /// <summary>ViewModel</summary>
+ public MqttViewModel ViewModel { get; }
+
+ /// <summary>实例化 MQTT 客户端窗口</summary>
+ public MqttWindow()
+ {
+ InitializeComponent();
+
+ ViewModel = new MqttViewModel();
+ DataContext = ViewModel;
+
+ // 绑定日志回调
+ ViewModel.OnLog += AppendLog;
+ }
+
+ private void AppendLog(String msg)
+ {
+ if (!Dispatcher.CheckAccess())
+ {
+ Dispatcher.Invoke(() => AppendLog(msg));
+ return;
+ }
+
+ if (msg == "__CLEAR__")
+ {
+ txtLog.Document.Blocks.Clear();
+ return;
+ }
+
+ var paragraph = new Paragraph(new Run(msg))
+ {
+ Margin = new Thickness(0),
+ Padding = new Thickness(0)
+ };
+
+ txtLog.Document.Blocks.Add(paragraph);
+ txtLog.ScrollToEnd();
+ }
+}
diff --git a/CrazyCoder/Views/NetworkWindow.xaml b/CrazyCoder/Views/NetworkWindow.xaml
new file mode 100644
index 0000000..fb02526
--- /dev/null
+++ b/CrazyCoder/Views/NetworkWindow.xaml
@@ -0,0 +1,305 @@
+<Window x:Class="CrazyCoder.Views.NetworkWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d"
+ Title="网络调试工具" Height="750" Width="1100" WindowStartupLocation="CenterScreen">
+ <Window.Resources>
+ <Style x:Key="GroupBorder" TargetType="Border">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,6"/>
+ </Style>
+
+ <Style x:Key="SectionTitle" TargetType="TextBlock">
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Setter Property="Margin" Value="4,3"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+
+ <Style x:Key="ConfigLabel" TargetType="TextBlock">
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ <Setter Property="Margin" Value="4,2"/>
+ </Style>
+
+ <Style x:Key="ConfigTextBox" TargetType="TextBox">
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ </Style>
+
+ <Style x:Key="OptionCheckBox" TargetType="CheckBox">
+ <Setter Property="Margin" Value="4,2"/>
+ </Style>
+
+ <Style x:Key="ActionButton" TargetType="Button">
+ <Setter Property="Height" Value="30"/>
+ <Setter Property="Margin" Value="2"/>
+ </Style>
+
+ <Style x:Key="SendButton" TargetType="Button">
+ <Setter Property="Height" Value="50"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="FontWeight" Value="Bold"/>
+ </Style>
+
+ <Style x:Key="SmallNumeric" TargetType="TextBox">
+ <Setter Property="Width" Value="60"/>
+ <Setter Property="Height" Value="26"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="HorizontalContentAlignment" Value="Right"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="Margin" Value="2"/>
+ </Style>
+ </Window.Resources>
+
+ <Grid Margin="6">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="300"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- ============ 左侧:配置面板 ============ -->
+ <ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" Margin="0,0,4,0">
+ <StackPanel>
+ <!-- 工作模式 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="工作模式" Style="{StaticResource SectionTitle}"/>
+ <ComboBox ItemsSource="{Binding Modes}" SelectedIndex="{Binding SelectedModeIndex}"
+ Margin="4,2" Height="30">
+ <ComboBox.Style>
+ <Style TargetType="ComboBox">
+ <Setter Property="IsEnabled" Value="True"/>
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding IsConnected}" Value="True">
+ <Setter Property="IsEnabled" Value="False"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </ComboBox.Style>
+ </ComboBox>
+ </StackPanel>
+ </Border>
+
+ <!-- 连接配置 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="连接配置" Style="{StaticResource SectionTitle}"/>
+
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="50"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="本地" Style="{StaticResource ConfigLabel}"/>
+ <ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding LocalAddresses}"
+ Text="{Binding LocalAddress, UpdateSourceTrigger=PropertyChanged}"
+ Height="26" Margin="2" IsEditable="True">
+ <ComboBox.Style>
+ <Style TargetType="ComboBox">
+ <Setter Property="IsEnabled" Value="True"/>
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding IsConnected}" Value="True">
+ <Setter Property="IsEnabled" Value="False"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </ComboBox.Style>
+ </ComboBox>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="远程" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding RemoteAddress, UpdateSourceTrigger=PropertyChanged}">
+ <TextBox.Style>
+ <Style TargetType="TextBox" BasedOn="{StaticResource ConfigTextBox}">
+ <Setter Property="IsEnabled" Value="True"/>
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding IsConnected}" Value="True">
+ <Setter Property="IsEnabled" Value="False"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </TextBox.Style>
+ </TextBox>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="端口" Style="{StaticResource ConfigLabel}"/>
+ <Grid Grid.Row="2" Grid.Column="1">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <TextBox Grid.Column="0" Text="{Binding Port, UpdateSourceTrigger=PropertyChanged}">
+ <TextBox.Style>
+ <Style TargetType="TextBox" BasedOn="{StaticResource ConfigTextBox}">
+ <Setter Property="IsEnabled" Value="True"/>
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding IsConnected}" Value="True">
+ <Setter Property="IsEnabled" Value="False"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </TextBox.Style>
+ </TextBox>
+ <Button Grid.Column="1" Content="{Binding ConnectButtonText}"
+ Command="{Binding ToggleConnectCommand}"
+ Width="80" Style="{StaticResource ActionButton}"
+ FontWeight="Bold" Margin="4,2"/>
+ </Grid>
+ </Grid>
+ </StackPanel>
+ </Border>
+
+ <!-- 日志配置 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="日志配置" Style="{StaticResource SectionTitle}"/>
+ <CheckBox Content="显示应用日志" IsChecked="{Binding ShowLog}" Style="{StaticResource OptionCheckBox}"/>
+ <CheckBox Content="显示网络日志" IsChecked="{Binding ShowSocketLog}" Style="{StaticResource OptionCheckBox}"/>
+ <CheckBox Content="显示接收字符串" IsChecked="{Binding ShowReceiveString}" Style="{StaticResource OptionCheckBox}"/>
+ <CheckBox Content="显示发送数据" IsChecked="{Binding ShowSend}" Style="{StaticResource OptionCheckBox}"/>
+ <CheckBox Content="显示接收数据" IsChecked="{Binding ShowReceive}" Style="{StaticResource OptionCheckBox}"/>
+ <CheckBox Content="显示统计信息" IsChecked="{Binding ShowStat}" Style="{StaticResource OptionCheckBox}"/>
+
+ <Button Content="清空接收日志" Command="{Binding ClearReceiveLogCommand}"
+ Style="{StaticResource ActionButton}" Margin="4,4"/>
+ </StackPanel>
+ </Border>
+
+ <!-- 发送配置 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="发送配置" Style="{StaticResource SectionTitle}"/>
+ <CheckBox Content="Hex发送" IsChecked="{Binding HexSend}" Style="{StaticResource OptionCheckBox}"/>
+
+ <Grid Margin="4,2">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="次数" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding SendTimes, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource SmallNumeric}" Width="Auto"/>
+ <TextBlock Grid.Row="0" Grid.Column="2" Text="次" Style="{StaticResource ConfigLabel}" Margin="2,2,4,2"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="间隔" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding SendSleep, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource SmallNumeric}" Width="Auto"/>
+ <TextBlock Grid.Row="1" Grid.Column="2" Text="毫秒" Style="{StaticResource ConfigLabel}" Margin="2,2,4,2"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="并发" Style="{StaticResource ConfigLabel}"/>
+ <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding SendThreads, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource SmallNumeric}" Width="Auto"/>
+ <TextBlock Grid.Row="2" Grid.Column="2" Text="连接" Style="{StaticResource ConfigLabel}" Margin="2,2,4,2"/>
+ </Grid>
+ </StackPanel>
+ </Border>
+
+ <!-- 统计信息 -->
+ <Border Style="{StaticResource GroupBorder}">
+ <StackPanel>
+ <TextBlock Text="统计信息" Style="{StaticResource SectionTitle}"/>
+ <Grid Margin="4,2">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="已接收" Style="{StaticResource ConfigLabel}"/>
+ <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding ReceivedBytes, StringFormat={}{0:N0} 字节}"
+ Style="{StaticResource ConfigLabel}" FontWeight="Bold"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="已发送" Style="{StaticResource ConfigLabel}"/>
+ <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding SentBytes, StringFormat={}{0:N0} 字节}"
+ Style="{StaticResource ConfigLabel}" FontWeight="Bold"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="连接数" Style="{StaticResource ConfigLabel}"/>
+ <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding SessionCount, StringFormat={}{0} 个}"
+ Style="{StaticResource ConfigLabel}" FontWeight="Bold"/>
+ </Grid>
+ </StackPanel>
+ </Border>
+ </StackPanel>
+ </ScrollViewer>
+
+ <!-- ============ 右侧:接收区 + 发送区 ============ -->
+ <Grid Grid.Column="1">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="3*"/>
+ <RowDefinition Height="4"/>
+ <RowDefinition Height="2*"/>
+ </Grid.RowDefinitions>
+
+ <!-- 接收日志区 -->
+ <Border Grid.Row="0" Style="{StaticResource GroupBorder}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Grid.Row="0" Text="接收日志" Style="{StaticResource SectionTitle}"/>
+ <RichTextBox x:Name="txtReceive" Grid.Row="1"
+ IsReadOnly="True" VerticalScrollBarVisibility="Auto"
+ FontFamily="Consolas" FontSize="12"
+ Background="#1E1E1E" Foreground="#D4D4D4"
+ BorderThickness="0" Margin="2"/>
+ </Grid>
+ </Border>
+
+ <GridSplitter Grid.Row="1" Height="4" HorizontalAlignment="Stretch" Background="#F0F0F0"/>
+
+ <!-- 发送区 -->
+ <Border Grid.Row="2" Style="{StaticResource GroupBorder}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <TextBlock Grid.Row="0" Text="发送区" Style="{StaticResource SectionTitle}"/>
+
+ <Grid Grid.Row="1" Margin="2">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="120"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBox Grid.Column="0"
+ Text="{Binding SendText, UpdateSourceTrigger=PropertyChanged}"
+ AcceptsReturn="True" AcceptsTab="True"
+ VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
+ FontFamily="Consolas" FontSize="13"
+ BorderThickness="1" BorderBrush="#D0D0D0"
+ Margin="0,0,4,0"/>
+
+ <Button Grid.Column="1" Content="发送"
+ Command="{Binding SendCommand}"
+ Style="{StaticResource SendButton}"
+ VerticalAlignment="Bottom"/>
+ </Grid>
+ </Grid>
+ </Border>
+ </Grid>
+ </Grid>
+</Window>
diff --git a/CrazyCoder/Views/NetworkWindow.xaml.cs b/CrazyCoder/Views/NetworkWindow.xaml.cs
new file mode 100644
index 0000000..b83b83c
--- /dev/null
+++ b/CrazyCoder/Views/NetworkWindow.xaml.cs
@@ -0,0 +1,49 @@
+using System.Windows;
+using System.Windows.Documents;
+using System.Windows.Media;
+using CrazyCoder.ViewModels;
+
+namespace CrazyCoder.Views;
+
+/// <summary>网络调试工具窗口</summary>
+public partial class NetworkWindow : Window
+{
+ /// <summary>ViewModel</summary>
+ public NetworkViewModel ViewModel { get; }
+
+ /// <summary>实例化网络调试工具窗口</summary>
+ public NetworkWindow()
+ {
+ InitializeComponent();
+
+ ViewModel = new NetworkViewModel();
+ DataContext = ViewModel;
+
+ // 绑定日志回调
+ ViewModel.OnLog += AppendLog;
+ }
+
+ private void AppendLog(String msg)
+ {
+ if (!Dispatcher.CheckAccess())
+ {
+ Dispatcher.Invoke(() => AppendLog(msg));
+ return;
+ }
+
+ if (msg == "__CLEAR__")
+ {
+ txtReceive.Document.Blocks.Clear();
+ return;
+ }
+
+ var paragraph = new Paragraph(new Run(msg))
+ {
+ Margin = new Thickness(0),
+ Padding = new Thickness(0)
+ };
+
+ txtReceive.Document.Blocks.Add(paragraph);
+ txtReceive.ScrollToEnd();
+ }
+}
diff --git a/CrazyCoder/Views/RegexWindow.xaml b/CrazyCoder/Views/RegexWindow.xaml
index 9a5465d..e2201bb 100644
--- a/CrazyCoder/Views/RegexWindow.xaml
+++ b/CrazyCoder/Views/RegexWindow.xaml
@@ -3,10 +3,220 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- xmlns:local="clr-namespace:CrazyCoder.Views"
+ xmlns:vm="clr-namespace:CrazyCoder.ViewModels"
+ xmlns:models="clr-namespace:CrazyCoder.Models"
mc:Ignorable="d"
- Title="正则表达式" Height="450" Width="800">
- <Grid>
- <TextBlock Text="正则表达式"/>
+ Title="正则表达式" Height="700" Width="1100" WindowStartupLocation="CenterScreen">
+ <Window.Resources>
+ <Style x:Key="ResultListView" TargetType="ListView">
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="Margin" Value="2"/>
+ </Style>
+
+ <Style x:Key="CodeTextBox" TargetType="TextBox">
+ <Setter Property="FontFamily" Value="Consolas"/>
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="AcceptsReturn" Value="True"/>
+ <Setter Property="AcceptsTab" Value="True"/>
+ <Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
+ <Setter Property="HorizontalScrollBarVisibility" Value="Auto"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ </Style>
+ </Window.Resources>
+
+ <Grid Margin="8">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="3*"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <!-- 第1行:正则表达式输入 -->
+ <Border Grid.Row="0" BorderBrush="#D0D0D0" BorderThickness="1" Background="#FFF5E6" Margin="0,0,0,4">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="正则表达式" FontWeight="Bold" Margin="4,2"/>
+ <TextBox Grid.Row="1" Text="{Binding Pattern, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource CodeTextBox}" Background="#FFF5E6" MinHeight="60"/>
+ </Grid>
+ </Border>
+
+ <!-- 第2行:选项 + 模式切换 + 执行按钮 -->
+ <Grid Grid.Row="1" Margin="0,0,0,4">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <CheckBox Grid.Column="0" Content="忽略大小写" IsChecked="{Binding IgnoreCase}" Margin="4,0,8,0" ToolTip="使用不区分大小写的匹配"/>
+ <CheckBox Grid.Column="1" Content="多行模式" IsChecked="{Binding Multiline}" Margin="0,0,8,0" ToolTip="^和$匹配每行的开头和末尾"/>
+ <CheckBox Grid.Column="2" Content="单行模式" IsChecked="{Binding Singleline}" Margin="0,0,8,0" ToolTip="句点(.)匹配每个字符"/>
+ <CheckBox Grid.Column="3" Content="忽略空白" IsChecked="{Binding IgnorePatternWhitespace}" Margin="0,0,8,0" ToolTip="排除模式中的空白并启用#注释"/>
+
+ <Separator Grid.Column="4" Style="{StaticResource {x:Static ToolBar.SeparatorStyleKey}}" Margin="4,0"/>
+
+ <RadioButton Grid.Column="5" Content="匹配" IsChecked="{Binding IsMatchMode}" Margin="8,0,4,0" VerticalAlignment="Center"/>
+ <RadioButton Grid.Column="6" Content="替换" IsChecked="{Binding IsReplaceMode}" Margin="4,0,8,0" VerticalAlignment="Center"/>
+
+ <Button Grid.Column="7" Content="{Binding ExecuteButtonText}" Command="{Binding ExecuteCommand}"
+ MinWidth="100" Height="28" HorizontalAlignment="Right" Margin="0,0,4,0"/>
+ </Grid>
+
+ <!-- 第3行:替换输入(替换模式可见) -->
+ <Border Grid.Row="2" BorderBrush="#D0D0D0" BorderThickness="1" Background="#E8FFE8" Margin="0,0,0,4"
+ Visibility="{Binding ReplacePanelVisibility}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="替换为" FontWeight="Bold" Margin="4,2"/>
+ <TextBox Grid.Row="1" Text="{Binding Replacement, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource CodeTextBox}" Background="#E8FFE8" MinHeight="40"/>
+ </Grid>
+ </Border>
+
+ <!-- 第4行:数据源输入 -->
+ <Border Grid.Row="3" BorderBrush="#D0D0D0" BorderThickness="1" Background="#FFFFF0" Margin="0,0,0,4">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="数据源" FontWeight="Bold" Margin="4,2"/>
+ <TextBox Grid.Row="1" x:Name="txtSource" Text="{Binding SourceText, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource CodeTextBox}" Background="#FFFFF0"/>
+ </Grid>
+ </Border>
+
+ <!-- 第5行:结果区域 -->
+ <Grid Grid.Row="4" Visibility="{Binding ResultPanelVisibility}">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- Match -->
+ <Border Grid.Column="0" BorderBrush="#D0D0D0" BorderThickness="1" Margin="0,0,2,0">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="匹配结果 (Match)" FontWeight="Bold" Background="#FFD0FF" Padding="4,2"/>
+ <ListView Grid.Row="1" x:Name="lvMatch" ItemsSource="{Binding Matches}" SelectedItem="{Binding SelectedMatch}"
+ Background="#FFD0FF" BorderThickness="0">
+ <ListView.View>
+ <GridView>
+ <GridViewColumn Header="序号" Width="40" DisplayMemberBinding="{Binding Index}"/>
+ <GridViewColumn Header="匹配项" Width="150" DisplayMemberBinding="{Binding Value}"/>
+ <GridViewColumn Header="位置" Width="120" DisplayMemberBinding="{Binding Location}"/>
+ </GridView>
+ </ListView.View>
+ </ListView>
+ </Grid>
+ </Border>
+
+ <!-- Group -->
+ <Border Grid.Column="1" BorderBrush="#D0D0D0" BorderThickness="1" Margin="1,0">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="分组 (Group)" FontWeight="Bold" Background="#D0FFD0" Padding="4,2"/>
+ <ListView Grid.Row="1" x:Name="lvGroup" ItemsSource="{Binding Groups}" SelectedItem="{Binding SelectedGroup}"
+ Background="#D0FFD0" BorderThickness="0">
+ <ListView.View>
+ <GridView>
+ <GridViewColumn Header="序号" Width="40" DisplayMemberBinding="{Binding Index}"/>
+ <GridViewColumn Header="名称" Width="60" DisplayMemberBinding="{Binding Name}"/>
+ <GridViewColumn Header="值" Width="130" DisplayMemberBinding="{Binding Value}"/>
+ <GridViewColumn Header="位置" Width="100" DisplayMemberBinding="{Binding Location}"/>
+ </GridView>
+ </ListView.View>
+ </ListView>
+ </Grid>
+ </Border>
+
+ <!-- Capture -->
+ <Border Grid.Column="2" BorderBrush="#D0D0D0" BorderThickness="1" Margin="2,0,0,0">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Text="捕获 (Capture)" FontWeight="Bold" Background="#D0D0FF" Padding="4,2"/>
+ <ListView Grid.Row="1" x:Name="lvCapture" ItemsSource="{Binding Captures}" SelectedItem="{Binding SelectedCapture}"
+ Background="#D0D0FF" BorderThickness="0">
+ <ListView.View>
+ <GridView>
+ <GridViewColumn Header="序号" Width="40" DisplayMemberBinding="{Binding Index}"/>
+ <GridViewColumn Header="值" Width="150" DisplayMemberBinding="{Binding Value}"/>
+ <GridViewColumn Header="位置" Width="100" DisplayMemberBinding="{Binding Location}"/>
+ </GridView>
+ </ListView.View>
+ </ListView>
+ </Grid>
+ </Border>
+ </Grid>
+
+ <!-- 第6行:批量替换 -->
+ <Grid Grid.Row="5" Margin="0,4" Visibility="{Binding BatchPanelVisibility}">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <Button Grid.Column="0" Content="打开目录" Command="{Binding BrowseDirectoryCommand}" MinWidth="80" Height="26" Margin="0,0,4,0"/>
+ <TextBox Grid.Column="1" Text="{Binding DirectoryPath, UpdateSourceTrigger=PropertyChanged}" IsReadOnly="True"
+ Background="#F0F0F0" VerticalAlignment="Center" Height="26" Margin="0,0,4,0"/>
+ <TextBlock Grid.Column="2" Text="过滤:" VerticalAlignment="Center" Margin="0,0,4,0"/>
+ <TextBox Grid.Column="3" Text="{Binding FileFilter, UpdateSourceTrigger=PropertyChanged}" Width="80" VerticalAlignment="Center" Height="26" Margin="0,0,4,0"/>
+ <Button Grid.Column="4" Content="全部替换" Command="{Binding BatchReplaceCommand}" MinWidth="80" Height="26"/>
+ </Grid>
+
+ <!-- 第7行:状态栏 -->
+ <StatusBar Grid.Row="6" Margin="0,2,0,0">
+ <StatusBar.ItemsPanel>
+ <ItemsPanelTemplate>
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ </Grid>
+ </ItemsPanelTemplate>
+ </StatusBar.ItemsPanel>
+ <StatusBarItem>
+ <TextBlock Text="选项:" FontWeight="Bold"/>
+ </StatusBarItem>
+ <StatusBarItem Grid.Column="1">
+ <TextBlock Text="{Binding OptionText}" Margin="4,0,0,0"/>
+ </StatusBarItem>
+ <StatusBarItem Grid.Column="2">
+ <TextBlock Text="{Binding Status}" Margin="4,0,0,0"/>
+ </StatusBarItem>
+ </StatusBar>
</Grid>
</Window>
diff --git a/CrazyCoder/Views/RegexWindow.xaml.cs b/CrazyCoder/Views/RegexWindow.xaml.cs
index 5265977..662cc1f 100644
--- a/CrazyCoder/Views/RegexWindow.xaml.cs
+++ b/CrazyCoder/Views/RegexWindow.xaml.cs
@@ -1,27 +1,72 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows;
+using System.Windows;
using System.Windows.Controls;
-using System.Windows.Data;
-using System.Windows.Documents;
-using System.Windows.Input;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using System.Windows.Shapes;
+using CrazyCoder.ViewModels;
namespace CrazyCoder.Views
{
- /// <summary>
- /// Regex.xaml 的交互逻辑
- /// </summary>
+ /// <summary>正则表达式工具窗口</summary>
public partial class RegexWindow : Window
{
+ /// <summary>ViewModel</summary>
+ public RegexViewModel ViewModel { get; }
+
+ /// <summary>实例化正则表达式工具窗口</summary>
public RegexWindow()
{
InitializeComponent();
+
+ ViewModel = new RegexViewModel();
+ DataContext = ViewModel;
+
+ // 选中匹配/分组/捕获项时在源文本中高亮
+ lvMatch.SelectionChanged += OnMatchSelectionChanged;
+ lvGroup.SelectionChanged += OnGroupSelectionChanged;
+ lvCapture.SelectionChanged += OnCaptureSelectionChanged;
+ }
+
+ /// <summary>匹配项选中变更 - 在源文本中高亮</summary>
+ private void OnMatchSelectionChanged(Object sender, SelectionChangedEventArgs e)
+ {
+ if (ViewModel.SelectedMatch is not { } match) return;
+ if (match.Match == null) return;
+
+ txtSource.Focus();
+ txtSource.Select(match.Position, match.Length);
+ txtSource.ScrollToLine(GetLineFromIndex(txtSource.Text, match.Position) - 1);
+ }
+
+ /// <summary>分组选中变更 - 在源文本中高亮</summary>
+ private void OnGroupSelectionChanged(Object sender, SelectionChangedEventArgs e)
+ {
+ if (ViewModel.SelectedGroup is not { } group) return;
+ if (group.Group == null) return;
+
+ txtSource.Focus();
+ txtSource.Select(group.Position, group.Length);
+ txtSource.ScrollToLine(GetLineFromIndex(txtSource.Text, group.Position) - 1);
+ }
+
+ /// <summary>捕获选中变更 - 在源文本中高亮</summary>
+ private void OnCaptureSelectionChanged(Object sender, SelectionChangedEventArgs e)
+ {
+ if (ViewModel.SelectedCapture is not { } capture) return;
+ if (capture.Capture == null) return;
+
+ txtSource.Focus();
+ txtSource.Select(capture.Position, capture.Length);
+ txtSource.ScrollToLine(GetLineFromIndex(txtSource.Text, capture.Position) - 1);
+ }
+
+ /// <summary>从索引计算行号(1-based)</summary>
+ private static Int32 GetLineFromIndex(String text, Int32 index)
+ {
+ if (String.IsNullOrEmpty(text) || index <= 0) return 1;
+ var line = 1;
+ for (var i = 0; i < index && i < text.Length; i++)
+ {
+ if (text[i] == '\n') line++;
+ }
+ return line;
}
}
}
diff --git a/CrazyCoder/Views/SecurityWindow.xaml b/CrazyCoder/Views/SecurityWindow.xaml
new file mode 100644
index 0000000..3d14198
--- /dev/null
+++ b/CrazyCoder/Views/SecurityWindow.xaml
@@ -0,0 +1,196 @@
+<Window x:Class="CrazyCoder.Views.SecurityWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d"
+ Title="加密解密" Height="750" Width="1100" WindowStartupLocation="CenterScreen">
+ <Window.Resources>
+ <Style x:Key="FuncButton" TargetType="Button">
+ <Setter Property="Width" Value="110"/>
+ <Setter Property="Height" Value="46"/>
+ <Setter Property="Margin" Value="3"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+
+ <Style x:Key="CodeTextBox" TargetType="TextBox">
+ <Setter Property="FontFamily" Value="Consolas"/>
+ <Setter Property="FontSize" Value="13"/>
+ <Setter Property="AcceptsReturn" Value="True"/>
+ <Setter Property="AcceptsTab" Value="True"/>
+ <Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
+ <Setter Property="HorizontalScrollBarVisibility" Value="Auto"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="TextWrapping" Value="Wrap"/>
+ </Style>
+
+ <Style x:Key="GroupBorder" TargetType="Border">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,4"/>
+ </Style>
+
+ <Style x:Key="OptionRadioButton" TargetType="RadioButton">
+ <Setter Property="Margin" Value="4,0,8,0"/>
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ </Style>
+
+ <Style x:Key="OptionCheckBox" TargetType="CheckBox">
+ <Setter Property="Margin" Value="4,0,8,0"/>
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ </Style>
+ </Window.Resources>
+
+ <Grid Margin="6">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="240"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- 左侧:功能按钮区 -->
+ <Border Grid.Column="0" BorderBrush="#D0D0D0" BorderThickness="1" Margin="0,0,4,0">
+ <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
+ <WrapPanel Orientation="Horizontal" Margin="4">
+ <Button Content="HEX编码" Style="{StaticResource FuncButton}" Command="{Binding HexEncodeCommand}"/>
+ <Button Content="HEX解码" Style="{StaticResource FuncButton}" Command="{Binding HexDecodeCommand}"/>
+ <Button Content="Base64编码" Style="{StaticResource FuncButton}" Command="{Binding Base64EncodeCommand}"/>
+ <Button Content="Base64解码" Style="{StaticResource FuncButton}" Command="{Binding Base64DecodeCommand}"/>
+ <Button Content="MD5_32" Style="{StaticResource FuncButton}" Command="{Binding MD5_32Command}"/>
+ <Button Content="MD5_16" Style="{StaticResource FuncButton}" Command="{Binding MD5_16Command}"/>
+ <Button Content="SHA1" Style="{StaticResource FuncButton}" Command="{Binding SHA1Command}"/>
+ <Button Content="SHA256" Style="{StaticResource FuncButton}" Command="{Binding SHA256Command}"/>
+ <Button Content="SHA384" Style="{StaticResource FuncButton}" Command="{Binding SHA384Command}"/>
+ <Button Content="SHA512" Style="{StaticResource FuncButton}" Command="{Binding SHA512Command}"/>
+ <Button Content="CRC_32" Style="{StaticResource FuncButton}" Command="{Binding CRC_32Command}"/>
+ <Button Content="CRC_16" Style="{StaticResource FuncButton}" Command="{Binding CRC_16Command}"/>
+ <Button Content="RSA加密" Style="{StaticResource FuncButton}" Command="{Binding RSAEncryptCommand}"/>
+ <Button Content="RSA解密" Style="{StaticResource FuncButton}" Command="{Binding RSADecryptCommand}"/>
+ <Button Content="DSA签名" Style="{StaticResource FuncButton}" Command="{Binding DSASignCommand}"/>
+ <Button Content="DSA验证" Style="{StaticResource FuncButton}" Command="{Binding DSAVerifyCommand}"/>
+ <Button Content="Url编码" Style="{StaticResource FuncButton}" Command="{Binding UrlEncodeCommand}"/>
+ <Button Content="Url解码" Style="{StaticResource FuncButton}" Command="{Binding UrlDecodeCommand}"/>
+ <Button Content="Html编码" Style="{StaticResource FuncButton}" Command="{Binding HtmlEncodeCommand}"/>
+ <Button Content="Html解码" Style="{StaticResource FuncButton}" Command="{Binding HtmlDecodeCommand}"/>
+ <Button Content="时间戳" Style="{StaticResource FuncButton}" Command="{Binding TimestampCommand}"/>
+ <Button Content="机器信息" Style="{StaticResource FuncButton}" Command="{Binding ComputerInfoCommand}"/>
+ <Button Content="雪花Id" Style="{StaticResource FuncButton}" Command="{Binding SnowflakeCommand}"/>
+ <Button Content="JWT令牌" Style="{StaticResource FuncButton}" Command="{Binding JwtTokenCommand}"/>
+ <Button Content="版本号" Style="{StaticResource FuncButton}" Command="{Binding VersionCommand}"/>
+ <Button Content="TraceId" Style="{StaticResource FuncButton}" Command="{Binding TraceIdCommand}"/>
+ </WrapPanel>
+ </ScrollViewer>
+ </Border>
+
+ <!-- 右侧:原文、密码、结果 -->
+ <Grid Grid.Column="1">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="3*"/>
+ <RowDefinition Height="2*"/>
+ <RowDefinition Height="3*"/>
+ </Grid.RowDefinitions>
+
+ <!-- 原文区 -->
+ <Border Grid.Row="0" Style="{StaticResource GroupBorder}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Column="0" Text="原文" FontWeight="Bold" Margin="4,2" VerticalAlignment="Center"/>
+
+ <RadioButton Grid.Column="1" Content="字符串" Style="{StaticResource OptionRadioButton}"
+ IsChecked="{Binding IsSourceString}" GroupName="SourceFormat"/>
+ <RadioButton Grid.Column="2" Content="HEX" Style="{StaticResource OptionRadioButton}"
+ IsChecked="{Binding IsSourceHex}" GroupName="SourceFormat"
+ IsEnabled="{Binding SourceHexEnabled}"/>
+ <RadioButton Grid.Column="3" Content="Base64" Style="{StaticResource OptionRadioButton}"
+ IsChecked="{Binding IsSourceBase64}" GroupName="SourceFormat"
+ IsEnabled="{Binding SourceBase64Enabled}"/>
+
+ <Button Grid.Column="5" Content="上下互换" Command="{Binding ExchangeCommand}"
+ MinWidth="80" Height="28" Margin="0,0,4,0"/>
+ </Grid>
+
+ <TextBox Grid.Row="1" x:Name="txtSource"
+ Text="{Binding SourceText, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource CodeTextBox}" MinHeight="80"/>
+ </Grid>
+ </Border>
+
+ <!-- 密码区 -->
+ <Border Grid.Row="1" Style="{StaticResource GroupBorder}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Column="0" Text="密码 / 时间基准" FontWeight="Bold" Margin="4,2" VerticalAlignment="Center"/>
+
+ <RadioButton Grid.Column="1" Content="字符串" Style="{StaticResource OptionRadioButton}"
+ IsChecked="{Binding IsPassString}" GroupName="PassFormat"/>
+ <RadioButton Grid.Column="2" Content="HEX" Style="{StaticResource OptionRadioButton}"
+ IsChecked="{Binding IsPassHex}" GroupName="PassFormat"/>
+ <RadioButton Grid.Column="3" Content="Base64" Style="{StaticResource OptionRadioButton}"
+ IsChecked="{Binding IsPassBase64}" GroupName="PassFormat"/>
+ </Grid>
+
+ <TextBox Grid.Row="1" Text="{Binding PassText, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource CodeTextBox}" MinHeight="50"/>
+ </Grid>
+ </Border>
+
+ <!-- 结果区 -->
+ <Border Grid.Row="2" Style="{StaticResource GroupBorder}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Column="0" Text="结果" FontWeight="Bold" Margin="4,2" VerticalAlignment="Center"/>
+
+ <CheckBox Grid.Column="1" Content="字符串" Style="{StaticResource OptionCheckBox}"
+ IsChecked="{Binding IsResultString}"/>
+ <CheckBox Grid.Column="2" Content="HEX" Style="{StaticResource OptionCheckBox}"
+ IsChecked="{Binding IsResultHex}"/>
+ <CheckBox Grid.Column="3" Content="Base64" Style="{StaticResource OptionCheckBox}"
+ IsChecked="{Binding IsResultBase64}"/>
+ </Grid>
+
+ <TextBox Grid.Row="1" x:Name="txtResult"
+ Text="{Binding ResultText, UpdateSourceTrigger=PropertyChanged}"
+ Style="{StaticResource CodeTextBox}" IsReadOnly="True" Background="#F5F5F5"/>
+ </Grid>
+ </Border>
+ </Grid>
+ </Grid>
+</Window>
diff --git a/CrazyCoder/Views/SecurityWindow.xaml.cs b/CrazyCoder/Views/SecurityWindow.xaml.cs
new file mode 100644
index 0000000..46debcb
--- /dev/null
+++ b/CrazyCoder/Views/SecurityWindow.xaml.cs
@@ -0,0 +1,16 @@
+using System.Windows;
+using CrazyCoder.ViewModels;
+
+namespace CrazyCoder.Views;
+
+/// <summary>加密解密工具窗口</summary>
+public partial class SecurityWindow : Window
+{
+ /// <summary>实例化加密解密工具窗口</summary>
+ public SecurityWindow()
+ {
+ InitializeComponent();
+
+ DataContext = new SecurityViewModel();
+ }
+}
diff --git a/CrazyCoder/Views/SpeechWindow.xaml b/CrazyCoder/Views/SpeechWindow.xaml
new file mode 100644
index 0000000..bd8bb8d
--- /dev/null
+++ b/CrazyCoder/Views/SpeechWindow.xaml
@@ -0,0 +1,128 @@
+<Window x:Class="CrazyCoder.Views.SpeechWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d"
+ Title="语音助手" Height="500" Width="700" WindowStartupLocation="CenterScreen">
+ <Window.Resources>
+ <Style x:Key="GroupBorder" TargetType="Border">
+ <Setter Property="BorderBrush" Value="#D0D0D0"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ <Setter Property="Margin" Value="0,0,0,6"/>
+ </Style>
+
+ <Style x:Key="SectionTitle" TargetType="TextBlock">
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Setter Property="Margin" Value="4,3"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+
+ <Style x:Key="ConfigLabel" TargetType="TextBlock">
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ <Setter Property="Margin" Value="4,2"/>
+ </Style>
+
+ <Style x:Key="ActionButton" TargetType="Button">
+ <Setter Property="Height" Value="34"/>
+ <Setter Property="Margin" Value="2"/>
+ <Setter Property="Padding" Value="12,0"/>
+ <Setter Property="FontSize" Value="13"/>
+ </Style>
+ </Window.Resources>
+
+ <Grid Margin="8">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <!-- 语音设置 -->
+ <Border Grid.Row="0" Style="{StaticResource GroupBorder}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <TextBlock Text="语音设置" Style="{StaticResource SectionTitle}"/>
+
+ <Grid Grid.Row="1" Margin="4,0,4,2">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Column="0" Text="语音角色" Style="{StaticResource ConfigLabel}" Width="70"/>
+ <ComboBox Grid.Column="1" ItemsSource="{Binding Voices}"
+ SelectedIndex="{Binding SelectedVoiceIndex}"
+ Height="28" Margin="2"/>
+ </Grid>
+
+ <Grid Grid.Row="2" Margin="4,0,4,4">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Column="0" Text="音量" Style="{StaticResource ConfigLabel}" Width="40"/>
+ <Slider Grid.Column="1" Minimum="0" Maximum="100"
+ Value="{Binding Volume}" Margin="4,0"
+ TickFrequency="10" IsSnapToTickEnabled="True"
+ VerticalAlignment="Center"/>
+ <TextBlock Grid.Column="2" Text="{Binding Volume, StringFormat={}{0}}"
+ Style="{StaticResource ConfigLabel}" Width="40"
+ TextAlignment="Center"/>
+
+ <TextBlock Grid.Column="0" Text="语速" Style="{StaticResource ConfigLabel}" Width="40"
+ Grid.ColumnSpan="2" Visibility="Collapsed"/>
+ <TextBlock Grid.Column="2" Text="语速" Style="{StaticResource ConfigLabel}" Width="40"
+ Margin="8,2,2,2"/>
+ <Slider Grid.Column="3" Minimum="-10" Maximum="10"
+ Value="{Binding Rate}" Margin="4,0"
+ TickFrequency="1" IsSnapToTickEnabled="True"
+ VerticalAlignment="Center"/>
+ <TextBlock Grid.Column="4" Text="{Binding Rate, StringFormat={}{0}}"
+ Style="{StaticResource ConfigLabel}" Width="40"
+ TextAlignment="Center"/>
+ </Grid>
+ </Grid>
+ </Border>
+
+ <!-- 文本输入 -->
+ <Border Grid.Row="1" Style="{StaticResource GroupBorder}">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <TextBlock Text="朗读文本" Style="{StaticResource SectionTitle}"/>
+
+ <TextBox Grid.Row="1" Text="{Binding SpeakText, UpdateSourceTrigger=PropertyChanged}"
+ AcceptsReturn="True" AcceptsTab="True"
+ VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
+ FontFamily="Consolas" FontSize="14"
+ BorderThickness="0" Margin="4"
+ MinHeight="100"/>
+ </Grid>
+ </Border>
+
+ <!-- 操作按钮 -->
+ <Border Grid.Row="3" Style="{StaticResource GroupBorder}" Padding="4">
+ <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+ <Button Content="朗读" Command="{Binding SpeakCommand}"
+ Style="{StaticResource ActionButton}" Width="100"/>
+ <Button Content="停止" Command="{Binding StopCommand}"
+ Style="{StaticResource ActionButton}" Width="100"/>
+ <Button Content="保存为 WAV" Command="{Binding SaveWavCommand}"
+ Style="{StaticResource ActionButton}" Width="120"/>
+ </StackPanel>
+ </Border>
+ </Grid>
+</Window>
diff --git a/CrazyCoder/Views/SpeechWindow.xaml.cs b/CrazyCoder/Views/SpeechWindow.xaml.cs
new file mode 100644
index 0000000..60a22bb
--- /dev/null
+++ b/CrazyCoder/Views/SpeechWindow.xaml.cs
@@ -0,0 +1,16 @@
+using System.Windows;
+using CrazyCoder.ViewModels;
+
+namespace CrazyCoder.Views;
+
+/// <summary>语音助手窗口</summary>
+public partial class SpeechWindow : Window
+{
+ /// <summary>实例化语音助手窗口</summary>
+ public SpeechWindow()
+ {
+ InitializeComponent();
+
+ DataContext = new SpeechViewModel();
+ }
+}