Some checks failed
Build (CI) / build (ARM64, CI) (push) Has been cancelled
Build (CI) / build (x64, CI) (push) Has been cancelled
3019 lines
152 KiB
VB.net
3019 lines
152 KiB
VB.net
Imports System.Globalization
|
||
Imports System.IO.Compression
|
||
Imports System.Runtime.CompilerServices
|
||
Imports System.Runtime.InteropServices
|
||
Imports System.Text.RegularExpressions
|
||
Imports System.Xaml
|
||
Imports System.Threading.Tasks
|
||
Imports Microsoft.Win32
|
||
Imports Newtonsoft.Json
|
||
Imports PCL.Core.App
|
||
Imports PCL.Core.Logging
|
||
Imports PCL.Core.Utils
|
||
Imports System.Windows
|
||
Imports PCL.Core.Utils.Codecs
|
||
Imports PCL.Core.Utils.OS
|
||
|
||
Public Module ModBase
|
||
|
||
#Region "声明"
|
||
|
||
'下列版本信息由更新器自动修改
|
||
Public Const VersionBaseName As String = "2.13.4-beta.1" '不含分支前缀的显示用版本名
|
||
Public Const VersionStandardCode As String = "2.13.4." & VersionBranchCode
|
||
Public Const UpstreamVersion As String = "2.10.5" '上游版本
|
||
Public ReadOnly CommitHash As String = If(EnvironmentInterop.GetSecret("GITHUB_SHA", False), "native") 'Commit Hash
|
||
Public ReadOnly CommitHashShort As String = If(CommitHash = "native", "native", CommitHash.Substring(0, 7)) 'Commit Hash,取前 7 位
|
||
Public Const VersionCode As Integer = 414 '内部版本号
|
||
'自动生成的版本信息
|
||
#If DEBUG Then
|
||
Public Const VersionBranchName As String = "Debug"
|
||
Public Const VersionBranchCode As String = "100"
|
||
#ElseIf DEBUGCI Then
|
||
Public Const VersionBranchName As String = "CI"
|
||
Public Const VersionBranchCode As String = "50"
|
||
#Else
|
||
Public Const VersionBranchName As String = "Publish"
|
||
Public Const VersionBranchCode As String = "0"
|
||
#End If
|
||
|
||
''' <summary>
|
||
''' 主窗口句柄。
|
||
''' </summary>
|
||
Public FrmHandle As IntPtr
|
||
'龙猫味石山小记: 用最不靠谱的实现写出能跑的代码 (AppDomain.CurrentDomain.SetupInformation.ApplicationBase 获取到的是当前工作目录而不是可执行文件所在目录)
|
||
''' <summary>
|
||
''' 程序可执行文件所在目录,以“\”结尾。
|
||
''' </summary>
|
||
Public ReadOnly ExePath As String = If(Basics.ExecutableDirectory.EndsWith("\"), Basics.ExecutableDirectory, Basics.ExecutableDirectory & "\")
|
||
''' <summary>
|
||
''' 程序可执行文件完整路径。
|
||
''' </summary>
|
||
Public ReadOnly ExePathWithName As String = Basics.ExecutablePath
|
||
''' <summary>
|
||
''' 程序内嵌图片文件夹路径,以“/”结尾。
|
||
''' </summary>
|
||
Public ReadOnly PathImage As String = "pack://application:,,,/Plain Craft Launcher 2;component/Images/"
|
||
''' <summary>
|
||
''' 当前程序的语言。
|
||
''' </summary>
|
||
Public Lang As String = "zh_CN"
|
||
''' <summary>
|
||
''' 设置对象。
|
||
''' </summary>
|
||
Public Setup As New ModSetup
|
||
''' <summary>
|
||
''' 程序的打开计时。
|
||
''' </summary>
|
||
Public ApplicationStartTick As Long = TimeUtils.GetTimeTick()
|
||
''' <summary>
|
||
''' 程序打开时的时间。
|
||
''' </summary>
|
||
Public ApplicationOpenTime As Date = Date.Now
|
||
''' <summary>
|
||
''' 识别码。
|
||
''' </summary>
|
||
Public UniqueAddress As String = SecretGetUniqueAddress()
|
||
''' <summary>
|
||
''' 程序是否已结束。
|
||
''' </summary>
|
||
Public IsProgramEnded As Boolean = False
|
||
''' <summary>
|
||
''' 是否为 32 位系统。
|
||
''' </summary>
|
||
Public Is32BitSystem As Boolean = Not Environment.Is64BitOperatingSystem
|
||
''' <summary>
|
||
''' 是否为 ARM64 架构。
|
||
''' </summary>
|
||
Public IsArm64System As Boolean = Runtime.InteropServices.RuntimeInformation.OSArchitecture = Runtime.InteropServices.Architecture.Arm64
|
||
''' <summary>
|
||
''' 是否使用 GBK 编码。
|
||
''' </summary>
|
||
Public IsGBKEncoding As Boolean = Encoding.Default.CodePage = 936
|
||
''' <summary>
|
||
''' 系统盘盘符,以 \ 结尾。例如 “C:\”。
|
||
''' </summary>
|
||
Public OsDrive As String = Environment.GetLogicalDrives().Where(Function(p) Directory.Exists(p)).First.ToUpper.First & ":\" '#3799
|
||
''' <summary>
|
||
''' 程序的缓存文件夹路径,以 \ 结尾。
|
||
''' </summary>
|
||
Public PathTemp As String = If(Setup.Get("SystemSystemCache") = "", IO.Path.GetTempPath() & "PCL\", Setup.Get("SystemSystemCache")).ToString.Replace("/", "\").TrimEnd("\") & "\"
|
||
''' <summary>
|
||
''' AppData 中的 PCL 文件夹路径,以 \ 结尾。
|
||
''' </summary>
|
||
Public PathAppdata As String = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\PCL\"
|
||
''' <summary>
|
||
''' AppData 中的 PCLCE 配置文件夹路径,以 \ 结尾。
|
||
''' </summary>
|
||
Public PathAppdataConfig As String = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & If(VersionBranchName = "Debug", "\.pclcedebug\", "\.pclce\")
|
||
|
||
Public PathHelpFolder As String = PathTemp & "CE\Help\"
|
||
|
||
#End Region
|
||
|
||
#Region "矢量图标"
|
||
|
||
Public Class Logo
|
||
''' <summary>
|
||
''' 图标按钮,心(空心),1.1x
|
||
''' </summary>
|
||
Public Const IconButtonLikeLine As String = "M512 896a42.666667 42.666667 0 0 1-30.293333-12.373333l-331.52-331.946667a224.426667 224.426667 0 0 1 0-315.733333 223.573333 223.573333 0 0 1 315.733333 0L512 282.026667l46.08-46.08a223.573333 223.573333 0 0 1 315.733333 0 224.426667 224.426667 0 0 1 0 315.733333l-331.52 331.946667A42.666667 42.666667 0 0 1 512 896zM308.053333 256a136.533333 136.533333 0 0 0-97.28 40.106667 138.24 138.24 0 0 0 0 194.986666L512 792.746667l301.226667-301.653334a138.24 138.24 0 0 0 0-194.986666 141.653333 141.653333 0 0 0-194.56 0l-76.373334 76.8a42.666667 42.666667 0 0 1-60.586666 0L405.333333 296.106667A136.533333 136.533333 0 0 0 308.053333 256z"
|
||
''' <summary>
|
||
''' 图标按钮,心(实心),1.1x
|
||
''' </summary>
|
||
Public Const IconButtonLikeFill As String = "M700.856 155.543c-74.769 0-144.295 72.696-190.046 127.26-45.737-54.576-115.247-127.26-190.056-127.26-134.79 0-244.443 105.78-244.443 235.799 0 77.57 39.278 131.988 70.845 175.713C238.908 694.053 469.62 852.094 479.39 858.757c9.41 6.414 20.424 9.629 31.401 9.629 11.006 0 21.998-3.215 31.398-9.63 9.782-6.662 240.514-164.703 332.238-291.701 31.587-43.724 70.874-98.143 70.874-175.713-0.001-130.02-109.656-235.8-244.445-235.8z m0 0"
|
||
''' <summary>
|
||
''' 图标按钮,垃圾桶,1.1x
|
||
''' </summary>
|
||
Public Const IconButtonDelete As String = "M520.192 0C408.43 0 317.44 82.87 313.563 186.734H52.736c-29.038 0-52.663 21.943-52.663 49.079s23.625 49.152 52.663 49.152h58.075v550.473c0 103.35 75.118 187.757 167.717 187.757h472.43c92.599 0 167.716-83.894 167.716-187.757V285.477h52.59c29.038 0 52.59-21.943 52.663-49.08-0.073-27.135-23.625-49.151-52.663-49.151H726.235C723.237 83.017 631.955 0 520.192 0zM404.846 177.957c3.803-50.03 50.176-89.015 107.447-89.015 57.197 0 103.57 38.985 106.788 89.015H404.92zM284.379 933.669c-33.353 0-69.997-39.351-69.997-95.525v-549.01H833.39v549.522c0 56.247-36.645 95.525-69.998 95.525H284.379v-0.512z M357.23 800.695a48.274 48.274 0 0 0 47.616-49.006V471.7a48.274 48.274 0 0 0-47.543-49.08 48.274 48.274 0 0 0-47.69 49.006V751.69c0 27.282 20.846 49.006 47.617 49.006z m166.62 0a48.274 48.274 0 0 0 47.688-49.006V471.7a48.274 48.274 0 0 0-47.689-49.08 48.274 48.274 0 0 0-47.543 49.006V751.69c0 27.282 21.431 49.006 47.543 49.006z m142.92 0a48.274 48.274 0 0 0 47.543-49.006V471.7a48.274 48.274 0 0 0-47.543-49.08 48.274 48.274 0 0 0-47.616 49.006V751.69c0 27.282 20.773 49.006 47.543 49.006z"
|
||
''' <summary>
|
||
''' 图标按钮,禁止,1x
|
||
''' </summary>
|
||
Public Const IconButtonStop As String = "M508 990.4c-261.6 0-474.4-212-474.4-474.4S246.4 41.6 508 41.6s474.4 212 474.4 474.4S769.6 990.4 508 990.4zM508 136.8c-209.6 0-379.2 169.6-379.2 379.2 0 209.6 169.6 379.2 379.2 379.2s379.2-169.6 379.2-379.2C887.2 306.4 717.6 136.8 508 136.8zM697.6 563.2 318.4 563.2c-26.4 0-47.2-21.6-47.2-47.2 0-26.4 21.6-47.2 47.2-47.2l379.2 0c26.4 0 47.2 21.6 47.2 47.2C744.8 542.4 724 563.2 697.6 563.2z"
|
||
''' <summary>
|
||
''' 图标按钮,勾选,1x
|
||
''' </summary>
|
||
Public Const IconButtonCheck As String = "M512 0a512 512 0 1 0 512 512A512 512 0 0 0 512 0z m0 921.6a409.6 409.6 0 1 1 409.6-409.6 409.6 409.6 0 0 1-409.6 409.6z M716.8 339.968l-256 253.44L328.192 460.8A51.2 51.2 0 0 0 256 532.992l168.448 168.96a51.2 51.2 0 0 0 72.704 0l289.28-289.792A51.2 51.2 0 0 0 716.8 339.968z"
|
||
''' <summary>
|
||
''' 图标按钮,笔,1x
|
||
''' </summary>
|
||
Public Const IconButtonEdit As String = "M732.64 64.32C688.576 21.216 613.696 21.216 569.6 64.32L120.128 499.52c-17.6 12.896-26.432 30.144-30.848 51.68L32 870.048c0 25.856 8.8 56 26.432 73.248 17.632 17.216 17.632 48.704 88.64 48.704h13.248l326.08-56c22.016-4.32 39.68-12.928 52.864-30.176l449.472-435.2c22.048-21.536 35.264-47.36 35.264-77.536 0-30.176-13.216-56-35.264-77.568l-256.096-251.2zM139.712 903.776l56-326.912 311.04-295.136 267.104 269.44-310.976 295.168-323.168 57.44zM844.576 467.84l-273.984-260.672 61.856-59.84c8.832-8.512 26.528-8.512 39.776 0l234.24 226.496c4.384 4.288 8.832 12.8 8.832 17.088s-4.416 8.544-8.864 12.8l-61.856 64.128z"
|
||
''' <summary>
|
||
''' 图标按钮,齿轮,1.1x
|
||
''' </summary>
|
||
Public Const IconButtonSetup As String = "M651.946667 1001.813333c-22.186667 0-42.666667-10.24-61.44-27.306666-23.893333-23.893333-49.493333-35.84-75.093334-35.84-29.013333 0-56.32 11.946667-73.386666 30.72v3.413333c-17.066667 17.066667-42.666667 27.306667-66.56 27.306667h-6.826667c-6.826667 0-11.946667-1.706667-15.36-1.706667l-6.826667-1.706667c-64.853333-20.48-121.173333-54.613333-168.96-98.986666-29.013333-23.893333-37.546667-63.146667-25.6-95.573334 8.533333-23.893333 5.12-51.2-10.24-75.093333-15.36-27.306667-34.133333-40.96-59.733333-47.786667h-1.706667l-5.12-1.706666c-35.84-8.533333-61.44-34.133333-66.56-69.973334C1.706667 575.146667 0 537.6 0 512c0-32.426667 3.413333-63.146667 8.533333-93.866667v-6.826666l3.413334-8.533334c10.24-23.893333 23.893333-40.96 44.373333-51.2 5.12-3.413333 11.946667-6.826667 20.48-8.533333 27.306667-8.533333 51.2-25.6 63.146667-44.373333 13.653333-23.893333 17.066667-52.906667 10.24-81.92-11.946667-34.133333 0-71.68 30.72-93.866667 44.373333-37.546667 97.28-68.266667 158.72-93.866667l3.413333-1.706666c44.373333-13.653333 75.093333 3.413333 92.16 20.48 23.893333 23.893333 49.493333 35.84 75.093333 35.84 30.72 0 56.32-10.24 71.68-30.72l3.413334-3.413334c27.306667-27.306667 63.146667-35.84 93.866666-22.186666 63.146667 22.186667 117.76 54.613333 165.546667 97.28 29.013333 23.893333 37.546667 63.146667 25.6 95.573333-8.533333 23.893333-5.12 51.2 10.24 75.093333 15.36 27.306667 34.133333 40.96 59.733333 47.786667h1.706667l5.12 1.706667c35.84 8.533333 61.44 34.133333 66.56 71.68 6.826667 30.72 10.24 63.146667 11.946667 93.866666v3.413334c0 32.426667-3.413333 63.146667-8.533334 93.866666v6.826667l-3.413333 8.533333c-10.24 23.893333-23.893333 40.96-44.373333 51.2-5.12 3.413333-11.946667 6.826667-20.48 8.533334-27.306667 8.533333-51.2 25.6-63.146667 46.08-13.653333 23.893333-17.066667 52.906667-10.24 81.92 11.946667 35.84-1.706667 75.093333-30.72 95.573333-44.373333 35.84-95.573333 66.56-157.013333 92.16-15.36 3.413333-27.306667 3.413333-35.84 3.413333z m3.413333-83.626666z m1.706667 0zM517.12 853.333333c47.786667 0 93.866667 20.48 134.826667 59.733334 1.706667 1.706667 3.413333 1.706667 3.413333 3.413333 52.906667-22.186667 97.28-49.493333 136.533333-80.213333l1.706667-1.706667v-3.413333c-13.653333-52.906667-8.533333-104.106667 17.066667-148.48 23.893333-39.253333 64.853333-69.973333 114.346666-85.333334 1.706667 0 3.413333-1.706667 6.826667-6.826666 5.12-25.6 8.533333-51.2 8.533333-78.506667-1.706667-29.013333-3.413333-56.32-10.24-81.92v-5.12h-1.706666c-51.2-11.946667-90.453333-39.253333-119.466667-87.04-27.306667-44.373333-34.133333-100.693333-17.066667-148.48l-1.706666-1.706667h-3.413334c-39.253333-35.84-85.333333-63.146667-136.533333-80.213333H648.533333s-1.706667 1.706667-3.413333 1.706667c-32.426667 39.253333-80.213333 59.733333-136.533333 59.733333-47.786667 0-93.866667-20.48-134.826667-59.733333l-1.706667-1.706667h-1.706666c-54.613333 22.186667-98.986667 49.493333-136.533334 80.213333l-1.706666 1.706667v3.413333c13.653333 52.906667 8.533333 104.106667-17.066667 148.48-23.893333 39.253333-64.853333 69.973333-114.346667 85.333334-1.706667 0-3.413333 1.706667-6.826666 6.826666-6.826667 25.6-8.533333 51.2-8.533334 78.506667 0 30.72 3.413333 58.026667 6.826667 76.8l1.706667 5.12h1.706666c51.2 11.946667 90.453333 39.253333 119.466667 87.04 27.306667 44.373333 34.133333 100.693333 17.066667 148.48l1.706666 1.706667 1.706667 1.706666c37.546667 35.84 83.626667 63.146667 134.826667 80.213334 1.706667 0 3.413333 0 3.413333 1.706666h1.706667s1.706667 0 5.12-1.706666c34.133333-37.546667 81.92-59.733333 136.533333-59.733334z m-6.826667-146.773333c-110.933333 0-199.68-85.333333-199.68-196.266667 0-109.226667 87.04-196.266667 199.68-196.266666s199.68 85.333333 199.68 196.266666c-1.706667 109.226667-88.746667 196.266667-199.68 196.266667z m0-307.2c-63.146667 0-114.346667 49.493333-114.346666 110.933333 0 63.146667 49.493333 110.933333 114.346666 110.933334 30.72 0 59.733333-11.946667 80.213334-32.426667 20.48-20.48 32.426667-49.493333 32.426666-78.506667 0-63.146667-49.493333-110.933333-112.64-110.933333z"
|
||
''' <summary>
|
||
''' 图标按钮,重置,0.9x
|
||
''' </summary>
|
||
Public Const IconButtonReset As String = "M667.6817627 313.65283203l-45.28564454 55.76660156L858.06933594 391.27124023 787.61950684 165.93066406l-56.01379395 69.01611328A354.47387695 354.47387695 0 0 0 520.89892578 165.93066406C324.87536621 165.93066406 165.93066406 324.43041992 165.93066406 519.91015625c0 195.52917481 158.94470215 353.97949219 354.96826172 353.97949219a355.06713867 355.06713867 0 0 0 331.73217774-227.66418458 50.52612305 50.52612305 0 0 0-29.21813966-65.25878905 50.77331543 50.77331543 0 0 0-65.50598144 29.16870117A253.61938477 253.61938477 0 0 1 520.94836426 772.78796387c-140.05920411 0-253.61938477-113.21411133-253.61938477-252.87780762 0-139.61425781 113.56018067-252.82836914 253.61938477-252.82836914 53.59130859 0 104.46350098 16.61132813 146.73339843 46.57104492"
|
||
''' <summary>
|
||
''' 图标按钮,刷新,0.85x
|
||
''' </summary>
|
||
Public Const IconButtonRefresh As String = "M875.52 148.48C783.36 56.32 655.36 0 512 0 291.84 0 107.52 138.24 30.72 332.8l122.88 46.08C204.8 230.4 348.16 128 512 128c107.52 0 199.68 40.96 271.36 112.64L640 384h384V0L875.52 148.48zM512 896c-107.52 0-199.68-40.96-271.36-112.64L384 640H0v384l148.48-148.48C240.64 967.68 368.64 1024 512 1024c220.16 0 404.48-138.24 481.28-332.8L870.4 645.12C819.2 793.6 675.84 896 512 896z"
|
||
''' <summary>
|
||
''' 图标按钮,软盘,1x
|
||
''' </summary>
|
||
Public Const IconButtonSave As String = "M819.392 0L1024 202.752v652.16a168.96 168.96 0 0 1-168.832 168.768h-104.192a47.296 47.296 0 0 1-10.752 0H283.776a47.232 47.232 0 0 1-10.752 0H168.832A168.96 168.96 0 0 1 0 854.912V168.768A168.96 168.96 0 0 1 168.832 0h650.56z m110.208 854.912V242.112l-149.12-147.776H168.896c-41.088 0-74.432 33.408-74.432 74.432v686.144c0 41.024 33.344 74.432 74.432 74.432h62.4v-190.528c0-33.408 27.136-60.544 60.544-60.544h440.448c33.408 0 60.544 27.136 60.544 60.544v190.528h62.4c41.088 0 74.432-33.408 74.432-74.432z m-604.032 74.432h372.864v-156.736H325.568v156.736z m403.52-596.48a47.168 47.168 0 1 1 0 94.336H287.872a47.168 47.168 0 1 1 0-94.336h441.216z m0-153.728a47.168 47.168 0 1 1 0 94.4H287.872a47.168 47.168 0 1 1 0-94.4h441.216z"
|
||
''' <summary>
|
||
''' 图标按钮,信息,1.05x
|
||
''' </summary>
|
||
Public Const IconButtonInfo As String = "M512 917.333333c223.861333 0 405.333333-181.472 405.333333-405.333333S735.861333 106.666667 512 106.666667 106.666667 288.138667 106.666667 512s181.472 405.333333 405.333333 405.333333z m0 106.666667C229.226667 1024 0 794.773333 0 512S229.226667 0 512 0s512 229.226667 512 512-229.226667 512-512 512z m-32-597.333333h64a21.333333 21.333333 0 0 1 21.333333 21.333333v320a21.333333 21.333333 0 0 1-21.333333 21.333333h-64a21.333333 21.333333 0 0 1-21.333333-21.333333V448a21.333333 21.333333 0 0 1 21.333333-21.333333z m0-192h64a21.333333 21.333333 0 0 1 21.333333 21.333333v64a21.333333 21.333333 0 0 1-21.333333 21.333333h-64a21.333333 21.333333 0 0 1-21.333333-21.333333v-64a21.333333 21.333333 0 0 1 21.333333-21.333333z"
|
||
''' <summary>
|
||
''' 图标按钮,列表,1x
|
||
''' </summary>
|
||
Public Const IconButtonList As String = "M384 128h640v128H384zM160 192m-96 0a96 96 0 1 0 192 0 96 96 0 1 0-192 0ZM384 448h640v128H384zM160 512m-96 0a96 96 0 1 0 192 0 96 96 0 1 0-192 0ZM384 768h640v128H384zM160 832m-96 0a96 96 0 1 0 192 0 96 96 0 1 0-192 0Z"
|
||
''' <summary>
|
||
''' 图标按钮,文件夹,1.15x
|
||
''' </summary>
|
||
Public Const IconButtonOpen As String = "M889.018182 418.909091H884.363636V316.509091a93.090909 93.090909 0 0 0-99.607272-89.832727h-302.545455l-93.090909-76.334546A46.545455 46.545455 0 0 0 358.865455 139.636364H146.152727A93.090909 93.090909 0 0 0 46.545455 229.469091V837.818182a46.545455 46.545455 0 0 0 46.545454 46.545454 46.545455 46.545455 0 0 0 16.756364-3.258181 109.381818 109.381818 0 0 0 25.134545 3.258181h586.472727a85.178182 85.178182 0 0 0 87.04-63.301818l163.374546-302.545454a46.545455 46.545455 0 0 0 5.585454-21.876364A82.385455 82.385455 0 0 0 889.018182 418.909091z m-744.727273-186.181818h198.283636l93.09091 76.334545a46.545455 46.545455 0 0 0 29.323636 10.705455h319.301818a12.101818 12.101818 0 0 1 6.516364 0V418.909091H302.545455a85.178182 85.178182 0 0 0-87.04 63.301818L139.636364 622.778182V232.727273a19.549091 19.549091 0 0 1 6.516363 0z m578.094546 552.029091a27.461818 27.461818 0 0 0-2.792728 6.516363H154.530909l147.083636-272.290909a27.461818 27.461818 0 0 0 2.792728-6.981818h565.061818z"
|
||
''' <summary>
|
||
''' 图标按钮,名片,1.1x
|
||
''' </summary>
|
||
Public Const IconButtonCard As String = "M834.5 684.1c-31.2-70.4-98.9-120.9-179.1-127.3 63.5-8.5 112.6-63 112.6-128.8 0-71.8-58.2-130-130-130s-130 58.2-130 130c0 65.9 49 120.3 112.6 128.8-80.2 6.4-148 57-179.1 127.3-8.7 19.7 6 42 27.6 42 12.1 0 22.7-7.5 27.7-18.5 24.3-53.9 78.5-91.5 141.3-91.5s117 37.6 141.3 91.5c5 11.1 15.6 18.5 27.7 18.5 21.4 0 36.1-22.3 27.4-42zM567.9 427.9c0-38.6 31.4-70 70-70s70 31.4 70 70-31.4 70-70 70-70-31.4-70-70zM460.3 347.9H216.9c-16.6 0-30 13.4-30 30s13.4 30 30 30h243.3c16.6 0 30-13.4 30-30 0.1-16.5-13.4-30-29.9-30zM367.4 459.6H216.9c-16.6 0-30 13.4-30 30s13.4 30 30 30h150.4c16.6 0 30-13.4 30-30 0.1-16.6-13.4-30-29.9-30zM297.4 571.2H217c-16.6 0-30 13.4-30 30s13.4 30 30 30h80.4c16.6 0 30-13.4 30-30 0-16.5-13.5-30-30-30zM900 236v552H124V236h776m0-60H124c-33.1 0-60 26.9-60 60v552c0 33.1 26.9 60 60 60h776c33.1 0 60-26.9 60-60V236c0-33.1-26.9-60-60-60z"
|
||
''' <summary>
|
||
''' 图标按钮,×,0.85x
|
||
''' </summary>
|
||
Public Const IconButtonCross As String = "F1 M 26.9166,22.1667L 37.9999,33.25L 49.0832,22.1668L 53.8332,26.9168L 42.7499,38L 53.8332,49.0834L 49.0833,53.8334L 37.9999,42.75L 26.9166,53.8334L 22.1666,49.0833L 33.25,38L 22.1667,26.9167L 26.9166,22.1667 Z"
|
||
''' <summary>
|
||
''' 图标按钮,验证,1.1x
|
||
''' </summary>
|
||
Public Const IconButtonAuth As String = "M511.488256 95.184408c35.310345 22.516742 95.184408 55.78011 167.34033 84.437781 75.738131 29.681159 148.405797 40.93953 191.392304 45.033483v353.615193c0 73.691154-50.662669 164.781609-136.123938 244.101949C649.65917 901.181409 558.568716 942.12094 512 942.12094c-46.568716 0-137.65917-40.93953-222.096952-119.748126C204.441779 742.54073 153.77911 651.450275 153.77911 577.247376v-353.103448c42.474763-4.093953 116.165917-15.352324 191.904048-45.545227 75.226387-30.192904 133.565217-63.456272 165.805098-83.414293M512 0c-4.093953 0-8.187906 1.535232-11.258371 3.582209l-14.84058 10.234882c-1.023488 0.511744-67.550225 47.592204-170.410794 88.531735-100.813593 39.916042-198.556722 41.963018-199.58021 41.963018l-25.075462 0.511744c-10.746627 0.511744-18.934533 8.187906-18.934533 18.422789v414.000999c0 216.97951 286.064968 446.24088 440.09995 446.24088s440.09995-229.261369 440.09995-445.729136V163.758121c0-10.234883-8.69965-18.422789-18.934533-18.422789l-24.563718-0.511744c-1.023488 0-98.766617-2.046977-199.58021-41.963018-103.372314-40.93953-170.410795-88.01999-170.922538-88.531734L523.258371 3.582209c-3.070465-2.558721-7.164418-3.582209-11.258371-3.582209z M743.308346 410.930535l-260.477761 260.477761c-15.864068 15.864068-41.963018 15.864068-57.827087 0l-144.823588-144.823588c-15.864068-15.864068-15.864068-41.963018 0-57.827087 8.187906-8.187906 18.422789-11.770115 29.169415-11.770115 10.234883 0 20.981509 4.093953 29.169416 11.770115l115.654173 115.654173L685.993003 352.591704c15.864068-15.864068 41.963018-15.864068 57.827087 0 15.352324 16.375812 15.352324 42.474763-0.511744 58.338831z"
|
||
''' <summary>
|
||
''' 图标按钮,第三方
|
||
''' </summary>
|
||
Public Const IconButtonThirdparty As String = "M865.004 167.069c-10.794-9.687-24.91-15.085-39.579-15.085-1.383 0-2.629 0-4.013 0.139-0.831 0.139-10.102 0.692-24.771 0.692-24.218 0-71.408-1.522-116.107-12.178-57.708-13.7-124.411-77.083-143.785-89.675-9.687-6.227-21.034-9.41-32.244-9.41-11.21 0-22.42 3.182-32.244 9.41-2.353 1.522-72.1 73.484-140.324 89.675-44.699 10.655-92.72 12.178-116.938 12.178-14.53 0-23.941-0.554-24.771-0.692-1.246-0.139-2.629-0.139-3.875-0.139-14.67 0-28.924 5.396-39.717 15.085-11.763 10.655-18.405 25.325-18.405 40.825v140.048c0 517.846 351.089 584.411 366.034 587.040 3.46 0.554 6.782 0.831 10.241 0.831 3.46 0 6.918-0.276 10.241-0.831 14.946-2.629 368.663-69.33 368.663-587.040v-139.911c0.139-15.5-6.642-30.446-18.405-40.962v0zM825.425 348.080c0 476.883-320.783 531.961-320.783 531.961s-318.291-55.078-318.291-531.961v-140.048c0 0 10.933 0.831 28.785 0.831 30.446 0 81.648-2.214 130.777-13.839 80.403-19.098 158.731-97.564 158.731-97.564s81.787 78.466 162.19 97.564c49.129 11.625 99.501 13.839 129.946 13.839 17.714 0 28.785-0.831 28.785-0.831l-0.139 140.048zM463.405 491.173z M349.925 603.958l66.841-15.085c10.102 54.663 40.962 81.925 92.72 81.925 57.43-1.383 87.045-29.476 88.429-84.14 0-50.373-35.289-75.421-105.728-75.421-17.299 0-30.998 0-40.962 0v-51.757c10.102 0 20.757 0 32.382 0 66.149 0 99.916-25.187 101.3-75.421-1.383-45.945-26.571-69.747-75.421-71.132-48.85 0-77.635 26.571-86.215 79.85l-64.766-15.085c18.683-76.252 70.438-114.308 155.27-114.308 87.738 2.906 134.373 40.962 140.187 114.308-1.383 53.279-30.998 87.738-88.429 103.514 63.244 13.008 97.009 49.542 101.3 110.019-4.29 81.925-56.878 124.411-157.486 127.316-87.461 1.246-140.739-36.811-159.422-114.585z"
|
||
''' <summary>
|
||
''' 图标按钮,用户,0.95x
|
||
''' </summary>
|
||
Public Const IconButtonUser As String = "M660.338 528.065c63.61-46.825 105.131-121.964 105.131-206.83 0-141.7-115.29-256.987-256.997-256.987-141.706 0-256.998 115.288-256.998 256.987 0 85.901 42.52 161.887 107.456 208.562-152.1 59.92-260.185 207.961-260.185 381.077 0 21.276 17.253 38.53 38.53 38.53 21.278 0 38.53-17.254 38.53-38.53 0-183.426 149.232-332.671 332.667-332.671 1.589 0 3.113-0.207 4.694-0.244 0.8 0.056 1.553 0.244 2.362 0.244 183.434 0 332.664 149.245 332.664 332.671 0 21.276 17.255 38.53 38.533 38.53 21.277 0 38.53-17.254 38.53-38.53 0-174.885-110.354-324.13-264.917-382.809z m-331.803-206.83c0-99.22 80.72-179.927 179.935-179.927s179.937 80.708 179.937 179.927c0 99.203-80.721 179.91-179.937 179.91s-179.935-80.708-179.935-179.91z"
|
||
''' <summary>
|
||
''' 图标按钮,盾牌,1x
|
||
''' </summary>
|
||
Public Const IconButtonShield As String = "M511.488256 95.184408c35.310345 22.516742 95.184408 55.78011 167.34033 84.437781 75.738131 29.681159 148.405797 40.93953 191.392304 45.033483v353.615193c0 73.691154-50.662669 164.781609-136.123938 244.101949C649.65917 901.181409 558.568716 942.12094 512 942.12094c-46.568716 0-137.65917-40.93953-222.096952-119.748126C204.441779 742.54073 153.77911 651.450275 153.77911 577.247376v-353.103448c42.474763-4.093953 116.165917-15.352324 191.904048-45.545227 75.226387-30.192904 133.565217-63.456272 165.805098-83.414293M512 0c-4.093953 0-8.187906 1.535232-11.258371 3.582209l-14.84058 10.234882c-1.023488 0.511744-67.550225 47.592204-170.410794 88.531735-100.813593 39.916042-198.556722 41.963018-199.58021 41.963018l-25.075462 0.511744c-10.746627 0.511744-18.934533 8.187906-18.934533 18.422789v414.000999c0 216.97951 286.064968 446.24088 440.09995 446.24088s440.09995-229.261369 440.09995-445.729136V163.758121c0-10.234883-8.69965-18.422789-18.934533-18.422789l-24.563718-0.511744c-1.023488 0-98.766617-2.046977-199.58021-41.963018-103.372314-40.93953-170.410795-88.01999-170.922538-88.531734L523.258371 3.582209c-3.070465-2.558721-7.164418-3.582209-11.258371-3.582209z M743.308346 410.930535l-260.477761 260.477761c-15.864068 15.864068-41.963018 15.864068-57.827087 0l-144.823588-144.823588c-15.864068-15.864068-15.864068-41.963018 0-57.827087 8.187906-8.187906 18.422789-11.770115 29.169415-11.770115 10.234883 0 20.981509 4.093953 29.169416 11.770115l115.654173 115.654173L685.993003 352.591704c15.864068-15.864068 41.963018-15.864068 57.827087 0 15.352324 16.375812 15.352324 42.474763-0.511744 58.338831z"
|
||
''' <summary>
|
||
''' 图标按钮,离线,0.85x
|
||
''' </summary>
|
||
Public Const IconButtonOffline As String = "M533.293176 788.841412a60.235294 60.235294 0 1 1 85.202824 85.202823l-42.616471 42.586353c-129.355294 129.385412-339.124706 129.385412-468.510117 0-129.385412-129.385412-129.385412-339.124706 0-468.510117l42.586353-42.616471a60.235294 60.235294 0 1 1 85.202823 85.202824l-42.61647 42.586352a210.823529 210.823529 0 1 0 298.164706 298.164706l42.586352-42.61647z m255.548236-255.548236l42.61647-42.586352a210.823529 210.823529 0 1 0-298.164706-298.164706l-42.586352 42.61647a60.235294 60.235294 0 1 1-85.202824-85.202823l42.616471-42.586353c129.355294-129.385412 339.124706-129.385412 468.510117 0 129.385412 129.385412 129.385412 339.124706 0 468.510117l-42.586353 42.616471a60.235294 60.235294 0 1 1-85.202823-85.202824zM192.542118 192.542118a60.235294 60.235294 0 0 1 85.202823 0l553.712941 553.712941a60.235294 60.235294 0 0 1-85.202823 85.202823L192.542118 277.744941a60.235294 60.235294 0 0 1 0-85.202823z"
|
||
''' <summary>
|
||
''' 图标,服务端,1x
|
||
''' </summary>
|
||
Public Const IconButtonServer As String = "M224 160a64 64 0 0 0-64 64v576a64 64 0 0 0 64 64h576a64 64 0 0 0 64-64V224a64 64 0 0 0-64-64H224z m0 384h576v256H224v-256z m192 96v64h320v-64H416z m-128 0v64h64v-64H288zM224 224h576v256H224V224z m192 96v64h320v-64H416z m-128 0v64h64v-64H288z"
|
||
''' <summary>
|
||
''' 图标按钮,复制
|
||
''' </summary>
|
||
Public Const IconButtonCopy As String = "M394.666667 106.666667h448a74.666667 74.666667 0 0 1 74.666666 74.666666v448a74.666667 74.666667 0 0 1-74.666666 74.666667H394.666667a74.666667 74.666667 0 0 1-74.666667-74.666667V181.333333a74.666667 74.666667 0 0 1 74.666667-74.666666z m0 64a10.666667 10.666667 0 0 0-10.666667 10.666666v448a10.666667 10.666667 0 0 0 10.666667 10.666667h448a10.666667 10.666667 0 0 0 10.666666-10.666667V181.333333a10.666667 10.666667 0 0 0-10.666666-10.666666H394.666667z m245.333333 597.333333a32 32 0 0 1 64 0v74.666667a74.666667 74.666667 0 0 1-74.666667 74.666666H181.333333a74.666667 74.666667 0 0 1-74.666666-74.666666V394.666667a74.666667 74.666667 0 0 1 74.666666-74.666667h74.666667a32 32 0 0 1 0 64h-74.666667a10.666667 10.666667 0 0 0-10.666666 10.666667v448a10.666667 10.666667 0 0 0 10.666666 10.666666h448a10.666667 10.666667 0 0 0 10.666667-10.666666v-74.666667z"
|
||
''' <summary>
|
||
''' 图标按钮,外链
|
||
''' </summary>
|
||
Public Const IconButtonlink As String = "M433.230769 74.830769a43.323077 43.323077 0 0 1 0 86.646154l-236.307692 0.157539a35.446154 35.446154 0 0 0-35.446154 35.446153v630.153847a35.446154 35.446154 0 0 0 35.446154 35.446153h630.153846a35.446154 35.446154 0 0 0 35.446154-35.446153V590.769231a43.323077 43.323077 0 1 1 86.646154 0v236.425846a122.092308 122.092308 0 0 1-122.092308 122.092308H196.923077a122.092308 122.092308 0 0 1-122.092308-122.092308v-630.153846a122.092308 122.092308 0 0 1 122.092308-122.092308z m452.923077 0a63.015385 63.015385 0 0 1 63.015385 63.015385V354.461538a43.323077 43.323077 0 0 1-43.323077 43.323077l-4.726154-0.236307A43.323077 43.323077 0 0 1 862.523077 354.461538l-0.039385-131.702153-287.074461 287.15323-90.072616 90.072616a43.323077 43.323077 0 1 1-61.243077-61.243077l90.033231-90.072616 287.113846-287.192615H669.538462a43.323077 43.323077 0 0 1-43.08677-38.596923L626.215385 118.153846A43.323077 43.323077 0 0 1 669.538462 74.830769z"
|
||
''' <summary>
|
||
''' 图标,音符,1x
|
||
''' </summary>
|
||
Public Const IconMusic As String = "M348.293565 716.53287V254.797913c0-41.672348 28.004174-78.358261 68.919652-90.37913L815.994435 40.826435c62.775652-18.610087 125.907478 26.579478 125.907478 89.933913v539.158261c8.013913 42.25113-8.94887 89.177043-47.014956 127.109565a232.848696 232.848696 0 0 1-170.785392 65.758609c-61.885217-2.938435-111.081739-33.435826-129.113043-80.050087-18.031304-46.614261-2.137043-102.177391 41.672348-145.853218a232.848696 232.848696 0 0 1 170.785391-65.80313c21.014261 1.024 40.514783 5.164522 57.878261 12.065391V233.338435c0-12.109913-10.551652-20.034783-20.569044-20.034783a24.620522 24.620522 0 0 0-5.787826 0.934957L439.785739 338.18713a19.545043 19.545043 0 0 0-14.825739 19.144348v438.984348H423.846957c11.53113 43.987478-5.164522 94.208-45.412174 134.322087a232.848696 232.848696 0 0 1-170.785392 65.758609c-61.885217-2.938435-111.081739-33.435826-129.113043-80.050087-18.031304-46.614261-2.137043-102.177391 41.672348-145.853218a232.848696 232.848696 0 0 1 170.785391-65.80313c20.791652 1.024 40.069565 5.075478 57.299478 11.842783z"
|
||
''' <summary>
|
||
''' 图标,播放,0.8x
|
||
''' </summary>
|
||
Public Const IconPlay As String = "M803.904 463.936a55.168 55.168 0 0 1 0 96.128l-463.616 264.448C302.848 845.888 256 819.136 256 776.448V247.616c0-42.752 46.848-69.44 84.288-48.064l463.616 264.384z"
|
||
''' <summary>
|
||
''' 图标,创建,0.9x
|
||
''' </summary>
|
||
Public Const IconButtonCreate As String = "M103.331925 384.978025l25.805736 0L129.137661 161.847132c0-18.313088 14.905478-33.718963 33.718963-33.718963l0.969071 0 253.006318 0c10.82044 0 20.218484 4.797259 26.500561 12.257162l117.579929 126.753869 297.819966 0c18.297738 0 33.736359 15.179724 33.736359 33.977859l0 0.952698 0 82.909292 25.547863 0c18.538215 0 34.187637 15.179724 34.187637 33.977859 0 2.163269-0.469698 3.617387-0.469698 5.539156l-54.437843 432.971086c-1.210571 10.382465-7.007601 19.056008-14.968923 24.352641-6.249331 5.765307-14.680351 9.624195-23.595394 9.624195l-0.969071 0-694.906773 0c-9.155521 0-17.344017-3.858888-23.626094-9.155521-8.67252-5.765307-14.453177-14.939247-15.389502-25.758664L69.597613 423.040922c-2.165316-18.313088 10.868535-35.414581 29.665647-38.062897L103.331925 384.978025 103.331925 384.978025zM196.576609 384.978025 196.576609 384.978025l627.938546 0 0-49.625234L546.461371 335.352791l0 0c-9.400091 0-18.329461-4.117784-25.048489-11.110035L402.363486 196.067514 196.576609 196.067514 196.576609 384.978025 196.576609 384.978025zM879.469767 452.916347 879.469767 452.916347l-20.267603 0-0.469698 0-0.969071 0-694.906773 0-0.984421 0-20.218484 0 45.781696 366.728382 646.218888 0L879.469767 452.916347 879.469767 452.916347z"
|
||
''' <summary>
|
||
''' 图标,分享,1x
|
||
''' </summary>
|
||
Public Const IconButtonShare As String = "M768.704 703.616c-35.648 0-67.904 14.72-91.136 38.304l-309.152-171.712c9.056-17.568 14.688-37.184 14.688-58.272 0-12.576-2.368-24.48-5.76-35.936l304.608-189.152c22.688 20.416 52.384 33.184 85.216 33.184 70.592 0 128-57.408 128-128s-57.408-128-128-128-128 57.408-128 128c0 14.56 2.976 28.352 7.456 41.408l-301.824 187.392c-23.136-22.784-54.784-36.928-89.728-36.928-70.592 0-128 57.408-128 128 0 70.592 57.408 128 128 128 25.664 0 49.504-7.744 69.568-20.8l321.216 178.4c-3.04 10.944-5.184 22.208-5.184 34.08 0 70.592 57.408 128 128 128s128-57.408 128-128S839.328 703.616 768.704 703.616zM767.2 128.032c35.296 0 64 28.704 64 64s-28.704 64-64 64-64-28.704-64-64S731.904 128.032 767.2 128.032zM191.136 511.936c0-35.296 28.704-64 64-64s64 28.704 64 64c0 35.296-28.704 64-64 64S191.136 547.232 191.136 511.936zM768.704 895.616c-35.296 0-64-28.704-64-64s28.704-64 64-64 64 28.704 64 64S804 895.616 768.704 895.616z"
|
||
''' <summary>
|
||
''' 图标,添加,1x
|
||
''' </summary>
|
||
Public Const IconButtonAdd As String = "M512.277 954.412c-118.89 0-230.659-46.078-314.73-129.73S67.12 629.666 67.12 511.222s46.327-229.744 130.398-313.427 195.82-129.73 314.73-129.73 230.659 46.078 314.72 129.73S957.397 392.81 957.397 511.183 911.078 740.96 826.97 824.642s-195.8 129.77-314.692 129.77z m0-822.784c-101.972 0-197.809 39.494-269.865 111.222s-111.7 166.997-111.7 268.373 39.653 196.695 111.67 268.335S410.246 890.78 512.248 890.78s197.809-39.484 269.865-111.222 111.7-166.998 111.67-268.374c-0.03-101.375-39.654-196.665-111.67-268.303S614.22 131.628 512.277 131.628z m222.585 347.8H544.073V288.64c-0.76-17.561-15.613-31.18-33.173-30.419-16.495 0.714-29.704 13.924-30.419 30.419v190.787H289.703c-17.56 0.761-31.179 15.614-30.419 33.174 0.715 16.494 13.924 29.703 30.42 30.418H480.48v190.788c0.761 17.56 15.614 31.179 33.174 30.419 16.494-0.715 29.703-13.925 30.418-30.42V543.02h190.788c17.56 0.762 32.413-12.857 33.173-30.418 0.762-17.561-12.858-32.414-30.419-33.174a31.683 31.683 0 0 0-2.753 0z"
|
||
''' <summary>
|
||
''' 图标,开始游戏,1x
|
||
''' </summary>
|
||
Public Const IconPlayGame As String = "M213.333333 65.386667a85.333333 85.333333 0 0 1 43.904 12.16L859.370667 438.826667a85.333333 85.333333 0 0 1 0 146.346666L257.237333 946.453333A85.333333 85.333333 0 0 1 128 873.28V150.72a85.333333 85.333333 0 0 1 85.333333-85.333333z m0 64a21.333333 21.333333 0 0 0-21.184 18.837333L192 150.72v722.56a21.333333 21.333333 0 0 0 30.101333 19.456l2.197334-1.152L826.453333 530.282667a21.333333 21.333333 0 0 0 2.048-35.178667l-2.048-1.386667L224.298667 132.416A21.333333 21.333333 0 0 0 213.333333 129.386667z"
|
||
Public Const IconButtonEnable As String = "M512 0a512 512 0 1 0 512 512A512 512 0 0 0 512 0z m0 921.6a409.6 409.6 0 1 1 409.6-409.6 409.6 409.6 0 0 1-409.6 409.6z M716.8 339.968l-256 253.44L328.192 460.8A51.2 51.2 0 0 0 256 532.992l168.448 168.96a51.2 51.2 0 0 0 72.704 0l289.28-289.792A51.2 51.2 0 0 0 716.8 339.968z"
|
||
Public Const IconButtonDisable As String = "M508 990.4c-261.6 0-474.4-212-474.4-474.4S246.4 41.6 508 41.6s474.4 212 474.4 474.4S769.6 990.4 508 990.4zM508 136.8c-209.6 0-379.2 169.6-379.2 379.2 0 209.6 169.6 379.2 379.2 379.2s379.2-169.6 379.2-379.2C887.2 306.4 717.6 136.8 508 136.8zM697.6 563.2 318.4 563.2c-26.4 0-47.2-21.6-47.2-47.2 0-26.4 21.6-47.2 47.2-47.2l379.2 0c26.4 0 47.2 21.6 47.2 47.2C744.8 542.4 724 563.2 697.6 563.2z"
|
||
End Class
|
||
|
||
#End Region
|
||
|
||
#Region "自定义类"
|
||
|
||
''' <summary>
|
||
''' 支持小数与常见类型隐式转换的颜色。
|
||
''' </summary>
|
||
Public Class MyColor
|
||
|
||
Public A As Double = 255
|
||
Public R As Double = 0
|
||
Public G As Double = 0
|
||
Public B As Double = 0
|
||
|
||
'类型转换
|
||
Public Shared Widening Operator CType(str As String) As MyColor
|
||
Return New MyColor(str)
|
||
End Operator
|
||
Public Shared Widening Operator CType(col As Color) As MyColor
|
||
Return New MyColor(col)
|
||
End Operator
|
||
Public Shared Widening Operator CType(conv As MyColor) As Color
|
||
Return Color.FromArgb(MathByte(conv.A), MathByte(conv.R), MathByte(conv.G), MathByte(conv.B))
|
||
End Operator
|
||
Public Shared Widening Operator CType(conv As MyColor) As System.Drawing.Color
|
||
Return System.Drawing.Color.FromArgb(MathByte(conv.A), MathByte(conv.R), MathByte(conv.G), MathByte(conv.B))
|
||
End Operator
|
||
Public Shared Widening Operator CType(bru As SolidColorBrush) As MyColor
|
||
Return New MyColor(bru.Color)
|
||
End Operator
|
||
Public Shared Widening Operator CType(conv As MyColor) As SolidColorBrush
|
||
Return New SolidColorBrush(Color.FromArgb(MathByte(conv.A), MathByte(conv.R), MathByte(conv.G), MathByte(conv.B)))
|
||
End Operator
|
||
Public Shared Widening Operator CType(bru As Brush) As MyColor
|
||
Return New MyColor(bru)
|
||
End Operator
|
||
Public Shared Widening Operator CType(conv As MyColor) As Brush
|
||
Return New SolidColorBrush(Color.FromArgb(MathByte(conv.A), MathByte(conv.R), MathByte(conv.G), MathByte(conv.B)))
|
||
End Operator
|
||
|
||
'颜色运算
|
||
Public Shared Operator +(a As MyColor, b As MyColor) As MyColor
|
||
Return New MyColor With {.A = a.A + b.A, .B = a.B + b.B, .G = a.G + b.G, .R = a.R + b.R}
|
||
End Operator
|
||
Public Shared Operator -(a As MyColor, b As MyColor) As MyColor
|
||
Return New MyColor With {.A = a.A - b.A, .B = a.B - b.B, .G = a.G - b.G, .R = a.R - b.R}
|
||
End Operator
|
||
Public Shared Operator *(a As MyColor, b As Double) As MyColor
|
||
Return New MyColor With {.A = a.A * b, .B = a.B * b, .G = a.G * b, .R = a.R * b}
|
||
End Operator
|
||
Public Shared Operator /(a As MyColor, b As Double) As MyColor
|
||
Return New MyColor With {.A = a.A / b, .B = a.B / b, .G = a.G / b, .R = a.R / b}
|
||
End Operator
|
||
Public Shared Operator =(a As MyColor, b As MyColor) As Boolean
|
||
If IsNothing(a) AndAlso IsNothing(b) Then Return True
|
||
If IsNothing(a) OrElse IsNothing(b) Then Return False
|
||
Return a.A = b.A AndAlso a.R = b.R AndAlso a.G = b.G AndAlso a.B = b.B
|
||
End Operator
|
||
Public Shared Operator <>(a As MyColor, b As MyColor) As Boolean
|
||
If IsNothing(a) AndAlso IsNothing(b) Then Return False
|
||
If IsNothing(a) OrElse IsNothing(b) Then Return True
|
||
Return Not (a.A = b.A AndAlso a.R = b.R AndAlso a.G = b.G AndAlso a.B = b.B)
|
||
End Operator
|
||
|
||
'构造函数
|
||
Public Sub New()
|
||
End Sub
|
||
Public Sub New(col As Color)
|
||
Me.A = col.A
|
||
Me.R = col.R
|
||
Me.G = col.G
|
||
Me.B = col.B
|
||
End Sub
|
||
Public Sub New(HexString As String)
|
||
Dim StringColor As Media.Color = ColorConverter.ConvertFromString(HexString)
|
||
A = StringColor.A
|
||
R = StringColor.R
|
||
G = StringColor.G
|
||
B = StringColor.B
|
||
End Sub
|
||
Public Sub New(newA As Double, col As MyColor)
|
||
Me.A = newA
|
||
Me.R = col.R
|
||
Me.G = col.G
|
||
Me.B = col.B
|
||
End Sub
|
||
Public Sub New(newR As Double, newG As Double, newB As Double)
|
||
Me.A = 255
|
||
Me.R = newR
|
||
Me.G = newG
|
||
Me.B = newB
|
||
End Sub
|
||
Public Sub New(newA As Double, newR As Double, newG As Double, newB As Double)
|
||
Me.A = newA
|
||
Me.R = newR
|
||
Me.G = newG
|
||
Me.B = newB
|
||
End Sub
|
||
Public Sub New(brush As Brush)
|
||
Dim Color As Color = CType(brush, SolidColorBrush).Color
|
||
A = Color.A
|
||
R = Color.R
|
||
G = Color.G
|
||
B = Color.B
|
||
End Sub
|
||
Public Sub New(brush As SolidColorBrush)
|
||
Dim Color As Color = brush.Color
|
||
A = Color.A
|
||
R = Color.R
|
||
G = Color.G
|
||
B = Color.B
|
||
End Sub
|
||
Public Sub New(obj As Object)
|
||
If obj Is Nothing Then
|
||
A = 255 : R = 255 : G = 255 : B = 255
|
||
Else
|
||
If TypeOf obj Is SolidColorBrush Then
|
||
'避免反复获取 Color 对象造成性能下降
|
||
Dim Color As Color = CType(obj, SolidColorBrush).Color
|
||
A = Color.A
|
||
R = Color.R
|
||
G = Color.G
|
||
B = Color.B
|
||
Else
|
||
A = obj.A
|
||
R = obj.R
|
||
G = obj.G
|
||
B = obj.B
|
||
End If
|
||
End If
|
||
End Sub
|
||
|
||
'HSL
|
||
Public Function Hue(v1 As Double, v2 As Double, vH As Double) As Double
|
||
If vH < 0 Then vH += 1
|
||
If vH > 1 Then vH -= 1
|
||
If vH < 0.16667 Then Return v1 + (v2 - v1) * 6 * vH
|
||
If vH < 0.5 Then Return v2
|
||
If vH < 0.66667 Then Return v1 + (v2 - v1) * (4 - vH * 6)
|
||
Return v1
|
||
End Function
|
||
Public Function FromHSL(sH As Double, sS As Double, sL As Double) As MyColor
|
||
If sS = 0 Then
|
||
R = sL * 2.55
|
||
G = R
|
||
B = R
|
||
Else
|
||
Dim H = sH / 360
|
||
Dim S = sS / 100
|
||
Dim L = sL / 100
|
||
S = If(L < 0.5, S * L + L, S * (1.0 - L) + L)
|
||
L = 2 * L - S
|
||
R = 255 * Hue(L, S, H + 1 / 3)
|
||
G = 255 * Hue(L, S, H)
|
||
B = 255 * Hue(L, S, H - 1 / 3)
|
||
End If
|
||
A = 255
|
||
Return Me
|
||
End Function
|
||
Public Function FromHSL2(sH As Double, sS As Double, sL As Double) As MyColor
|
||
If sS = 0 Then
|
||
R = sL * 2.55 : G = R : B = R
|
||
Else
|
||
'初始化
|
||
sH = (sH + 3600000) Mod 360
|
||
Dim cent As Double() = {
|
||
+0.1, -0.06, -0.3, '0, 30, 60
|
||
-0.19, -0.15, -0.24, '90, 120, 150
|
||
-0.32, -0.09, +0.18, '180, 210, 240
|
||
+0.05, -0.12, -0.02, '270, 300, 330
|
||
+0.1, -0.06} '最后两位与前两位一致,加是变亮,减是变暗
|
||
'计算色调对应的亮度片区
|
||
Dim center As Double = sH / 30.0
|
||
Dim intCenter As Integer = Math.Floor(center) '亮度片区编号
|
||
center = 50 - (
|
||
(1 - center + intCenter) * cent(intCenter) + (center - intCenter) * cent(intCenter + 1)
|
||
) * sS
|
||
'center = 50 + (cent(intCenter) + (center - intCenter) * (cent(intCenter + 1) - cent(intCenter))) * sS
|
||
sL = If(sL < center, sL / center, 1 + (sL - center) / (100 - center)) * 50
|
||
FromHSL(sH, sS, sL)
|
||
End If
|
||
A = 255
|
||
Return Me
|
||
End Function
|
||
|
||
Public Function Alpha(sA As Double) As MyColor
|
||
A = sA
|
||
Return Me
|
||
End Function
|
||
|
||
Public Overrides Function ToString() As String
|
||
Return "(" & A & "," & R & "," & G & "," & B & ")"
|
||
End Function
|
||
Public Overrides Function Equals(obj As Object) As Boolean
|
||
Return Me = obj
|
||
End Function
|
||
|
||
End Class
|
||
|
||
''' <summary>
|
||
''' 支持负数与浮点数的矩形。
|
||
''' </summary>
|
||
Public Class MyRect
|
||
|
||
'属性
|
||
Public Property Width As Double = 0
|
||
Public Property Height As Double = 0
|
||
Public Property Left As Double = 0
|
||
Public Property Top As Double = 0
|
||
|
||
'构造函数
|
||
Public Sub New()
|
||
End Sub
|
||
Public Sub New(left As Double, top As Double, width As Double, height As Double)
|
||
Me.Left = left
|
||
Me.Top = top
|
||
Me.Width = width
|
||
Me.Height = height
|
||
End Sub
|
||
|
||
End Class
|
||
|
||
''' <summary>
|
||
''' 模块加载状态枚举。
|
||
''' </summary>
|
||
Public Enum LoadState
|
||
Waiting
|
||
Loading
|
||
Finished
|
||
Failed
|
||
Aborted
|
||
End Enum
|
||
|
||
''' <summary>
|
||
''' 执行返回值。
|
||
''' </summary>
|
||
Public Enum ProcessReturnValues
|
||
''' <summary>
|
||
''' 执行成功,或进程被中断。
|
||
''' </summary>
|
||
Aborted = -1
|
||
''' <summary>
|
||
''' 执行成功。
|
||
''' </summary>
|
||
Success = 0
|
||
''' <summary>
|
||
''' 执行失败。
|
||
''' </summary>
|
||
Fail = 1
|
||
''' <summary>
|
||
''' 执行时出现未经处理的异常。
|
||
''' </summary>
|
||
Exception = 2
|
||
''' <summary>
|
||
''' 执行超时。
|
||
''' </summary>
|
||
Timeout = 3
|
||
''' <summary>
|
||
''' 取消执行。可能是由于不满足执行的前置条件。
|
||
''' </summary>
|
||
Cancel = 4
|
||
''' <summary>
|
||
''' 任务成功完成。
|
||
''' </summary>
|
||
TaskDone = 5
|
||
End Enum
|
||
|
||
''' <summary>
|
||
''' 可以使用 Equals 和等号的 List。
|
||
''' </summary>
|
||
Public Class EqualableList(Of T)
|
||
Inherits List(Of T)
|
||
Public Overrides Function Equals(obj As Object) As Boolean
|
||
If TryCast(obj, List(Of T)) Is Nothing Then
|
||
'类型不同
|
||
Return False
|
||
Else
|
||
'类型相同
|
||
Dim objList As List(Of T) = obj
|
||
If objList.Count <> Count Then Return False
|
||
For i = 0 To objList.Count - 1
|
||
If Not objList(i).Equals(Me(i)) Then Return False
|
||
Next
|
||
Return True
|
||
End If
|
||
End Function
|
||
Public Shared Operator =(left As EqualableList(Of T), right As EqualableList(Of T)) As Boolean
|
||
Return EqualityComparer(Of EqualableList(Of T)).Default.Equals(left, right)
|
||
End Operator
|
||
Public Shared Operator <>(left As EqualableList(Of T), right As EqualableList(Of T)) As Boolean
|
||
Return Not left = right
|
||
End Operator
|
||
End Class
|
||
|
||
#End Region
|
||
|
||
#Region "数学"
|
||
|
||
''' <summary>
|
||
''' 2~65 进制的转换。
|
||
''' </summary>
|
||
Public Function RadixConvert(Input As String, FromRadix As Integer, ToRadix As Integer) As String
|
||
Const Digits As String = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz/+="
|
||
'零与负数的处理
|
||
If String.IsNullOrEmpty(Input) Then Return "0"
|
||
Dim IsNegative As Boolean = Input.StartsWithF("-")
|
||
If IsNegative Then Input = Input.TrimStart("-")
|
||
'转换为十进制
|
||
Dim RealNum As Long = 0, Scale As Long = 1
|
||
For Each Digit In Input.Reverse.Select(Function(l) Digits.IndexOfF(l))
|
||
RealNum += Digit * Scale
|
||
Scale *= FromRadix
|
||
Next
|
||
'转换为指定进制
|
||
Dim Result = ""
|
||
While RealNum > 0
|
||
Dim NewNum As Integer = RealNum Mod ToRadix
|
||
RealNum = (RealNum - NewNum) / ToRadix
|
||
Result = Digits(NewNum) & Result
|
||
End While
|
||
'负数的结束处理与返回
|
||
Return If(IsNegative, "-", "") & Result
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 计算二阶贝塞尔曲线。
|
||
''' </summary>
|
||
Public Function MathBezier(x As Double, x1 As Double, y1 As Double, x2 As Double, y2 As Double, Optional acc As Double = 0.01) As Double
|
||
If x <= 0 OrElse Double.IsNaN(x) Then Return 0
|
||
If x >= 1 Then Return 1
|
||
Dim a, b
|
||
a = x
|
||
Do
|
||
b = 3 * a * ((0.33333333 + x1 - x2) * a * a + (x2 - 2 * x1) * a + x1)
|
||
a += (x - b) * 0.5
|
||
Loop Until Math.Abs(b - x) < acc '精度
|
||
Return 3 * a * ((0.33333333 + y1 - y2) * a * a + (y2 - 2 * y1) * a + y1)
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 将一个数字限制为 0~255 的 Byte 值。
|
||
''' </summary>
|
||
Public Function MathByte(d As Double) As Byte
|
||
If d < 0 Then d = 0
|
||
If d > 255 Then d = 255
|
||
Return Math.Round(d)
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 提供 MyColor 类型支持的 Math.Round。
|
||
''' </summary>
|
||
Public Function MathRound(col As MyColor, Optional w As Integer = 0) As MyColor
|
||
Return New MyColor With {.A = Math.Round(col.A, w), .R = Math.Round(col.R, w), .G = Math.Round(col.G, w), .B = Math.Round(col.B, w)}
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 获取两数间的百分比。小数点精确到 6 位。
|
||
''' </summary>
|
||
''' <returns></returns>
|
||
Public Function MathPercent(ValueA As Double, ValueB As Double, Percent As Double) As Double
|
||
Return Math.Round(ValueA * (1 - Percent) + ValueB * Percent, 6) '解决 Double 计算错误
|
||
End Function
|
||
''' <summary>
|
||
''' 获取两颜色间的百分比,根据 RGB 计算。小数点精确到 6 位。
|
||
''' </summary>
|
||
Public Function MathPercent(ValueA As MyColor, ValueB As MyColor, Percent As Double) As MyColor
|
||
Return MathRound(ValueA * (1 - Percent) + ValueB * Percent, 6) '解决Double计算错误
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 将数值限定在某个范围内。
|
||
''' </summary>
|
||
Public Function MathClamp(value As Double, min As Double, max As Double) As Double
|
||
Return Math.Max(min, Math.Min(max, value))
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 符号函数。
|
||
''' </summary>
|
||
Public Function MathSgn(Value As Double) As Integer
|
||
If Value = 0 Then
|
||
Return 0
|
||
ElseIf Value > 0 Then
|
||
Return 1
|
||
Else
|
||
Return -1
|
||
End If
|
||
End Function
|
||
|
||
#End Region
|
||
|
||
#Region "文件"
|
||
|
||
'=============================
|
||
' 注册表
|
||
'=============================
|
||
|
||
''' <summary>
|
||
''' 重命名一个注册表子键。不可用于包含子键的子键。
|
||
''' </summary>
|
||
Public Sub RenameReg(parentKey As Microsoft.Win32.RegistryKey, subKeyName As String, newSubKeyName As String)
|
||
If parentKey.GetSubKeyNames().Contains(newSubKeyName) Then parentKey.DeleteSubKeyTree(newSubKeyName, False)
|
||
Dim SourceKey As Microsoft.Win32.RegistryKey = parentKey.OpenSubKey(subKeyName)
|
||
If IsNothing(SourceKey) Then Exit Sub '没有目标项
|
||
Dim NewKey As Microsoft.Win32.RegistryKey = parentKey.CreateSubKey(newSubKeyName)
|
||
If SourceKey.GetSubKeyNames().Length > 0 Then Throw New NotSupportedException("不支持对包含子键的子键进行重命名:" & SourceKey.GetSubKeyNames()(0) & "。")
|
||
For Each valueName As String In SourceKey.GetValueNames()
|
||
Dim objValue As Object = SourceKey.GetValue(valueName)
|
||
Dim valKind As Microsoft.Win32.RegistryValueKind = SourceKey.GetValueKind(valueName)
|
||
NewKey.SetValue(valueName, objValue, valKind)
|
||
Next
|
||
parentKey.DeleteSubKeyTree(subKeyName, False)
|
||
End Sub
|
||
''' <summary>
|
||
''' 读取注册表,默认为程序所属。
|
||
''' </summary>
|
||
Public Function ReadReg(Key As String, Optional DefaultValue As String = "", Optional Path As String = "") As String
|
||
Try
|
||
Dim parentKey As RegistryKey, softKey As RegistryKey
|
||
parentKey = Registry.CurrentUser
|
||
softKey = parentKey.OpenSubKey("Software\" & If(Path = "", RegFolder, Path), True)
|
||
If softKey Is Nothing Then
|
||
ReadReg = DefaultValue '不存在则返回默认值
|
||
Else
|
||
Dim readValue As New Text.StringBuilder
|
||
readValue.AppendLine(softKey.GetValue(Key))
|
||
Dim value = readValue.ToString.Replace(vbCrLf, "") '去除莫名的回车
|
||
Return If(value = "", DefaultValue, value) '错误则返回默认值
|
||
End If
|
||
Catch ex As Exception
|
||
Log(ex, "读取注册表出错:" & Key, LogLevel.Hint)
|
||
Return DefaultValue
|
||
End Try
|
||
End Function
|
||
''' <summary>
|
||
''' 写入注册表,默认为程序所属。
|
||
''' </summary>
|
||
Public Sub WriteReg(Key As String, Value As String, Optional ShowException As Boolean = False, Optional Path As String = "", Optional ThrowException As Boolean = False)
|
||
Try
|
||
Dim parentKey As RegistryKey, softKey As RegistryKey
|
||
parentKey = Registry.CurrentUser
|
||
softKey = parentKey.OpenSubKey("Software\" & If(Path = "", RegFolder, Path), True)
|
||
If softKey Is Nothing Then softKey = parentKey.CreateSubKey("Software\" & If(Path = "", RegFolder, Path)) '如果不存在就创建
|
||
softKey.SetValue(Key, Value)
|
||
Catch ex As Exception
|
||
Log(ex, "写入注册表出错:" & Key, If(ThrowException, LogLevel.Hint, LogLevel.Developer))
|
||
If ThrowException Then Throw
|
||
End Try
|
||
End Sub
|
||
''' <summary>
|
||
''' 是否存在某个注册表键。
|
||
''' </summary>
|
||
Public Function HasReg(Key As String) As Boolean
|
||
Return ReadReg(Key, Nothing) IsNot Nothing
|
||
End Function
|
||
''' <summary>
|
||
''' 删除注册表键。
|
||
''' </summary>
|
||
Public Sub DeleteReg(Key As String, Optional ThrowException As Boolean = False)
|
||
Try
|
||
Dim SubKey As Microsoft.Win32.RegistryKey = Registry.CurrentUser.OpenSubKey("Software\" & RegFolder, True)
|
||
SubKey?.DeleteValue(Key)
|
||
Catch ex As Exception
|
||
Log(ex, "删除注册表出错:" & Key, If(ThrowException, LogLevel.Hint, LogLevel.Developer))
|
||
If ThrowException Then Throw
|
||
End Try
|
||
End Sub
|
||
|
||
'=============================
|
||
' ini
|
||
'=============================
|
||
|
||
Private ReadOnly IniCache As New SafeDictionary(Of String, SafeDictionary(Of String, String))
|
||
''' <summary>
|
||
''' 清除某 ini 文件的运行时缓存。
|
||
''' </summary>
|
||
''' <param name="FileName">文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。</param>
|
||
Public Sub IniClearCache(FileName As String)
|
||
If Not FileName.Contains(":\") Then FileName = $"{ExePath}PCL\{FileName}.ini"
|
||
If IniCache.ContainsKey(FileName) Then IniCache.Remove(FileName)
|
||
End Sub
|
||
''' <summary>
|
||
''' 获取 ini 文件缓存。如果没有,则新读取 ini 文件内容。
|
||
''' 在文件不存在或读取失败时返回 Nothing。
|
||
''' </summary>
|
||
''' <param name="FileName">文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。</param>
|
||
Private Function IniGetContent(FileName As String) As SafeDictionary(Of String, String)
|
||
Try
|
||
'还原文件路径
|
||
If Not FileName.Contains(":\") Then FileName = $"{ExePath}PCL\{FileName}.ini"
|
||
'检索缓存
|
||
If IniCache.ContainsKey(FileName) Then Return IniCache(FileName)
|
||
'读取文件
|
||
If Not File.Exists(FileName) Then Return Nothing
|
||
Dim Ini As New SafeDictionary(Of String, String)
|
||
For Each Line In ReadFile(FileName).Split(vbCrLf.ToArray(), StringSplitOptions.RemoveEmptyEntries)
|
||
Dim Index As Integer = Line.IndexOfF(":")
|
||
If Index > 0 Then Ini(Line.Substring(0, Index)) = Line.Substring(Index + 1) '可能会有重复键,见 #3616
|
||
Next
|
||
IniCache(FileName) = Ini
|
||
Return Ini
|
||
Catch ex As Exception
|
||
Log(ex, $"生成 ini 文件缓存失败({FileName})", LogLevel.Hint)
|
||
Return Nothing
|
||
End Try
|
||
End Function
|
||
''' <summary>
|
||
''' 读取 ini 文件。这可能会使用到缓存。
|
||
''' </summary>
|
||
''' <param name="FileName">文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。</param>
|
||
''' <param name="Key">键。</param>
|
||
''' <param name="DefaultValue">没有找到键时返回的默认值。</param>
|
||
Public Function ReadIni(FileName As String, Key As String, Optional DefaultValue As String = "") As String
|
||
Dim Content = IniGetContent(FileName)
|
||
If Content Is Nothing OrElse Not Content.ContainsKey(Key) Then Return DefaultValue
|
||
Return Content(Key)
|
||
End Function
|
||
''' <summary>
|
||
''' 判断 ini 文件中是否包含某个键。这可能会使用到缓存。
|
||
''' </summary>
|
||
Public Function HasIniKey(FileName As String, Key As String) As Boolean
|
||
Dim Content = IniGetContent(FileName)
|
||
Return Content IsNot Nothing AndAlso Content.ContainsKey(Key)
|
||
End Function
|
||
''' <summary>
|
||
''' 从 ini 文件中移除某个键。这会更新缓存。
|
||
''' </summary>
|
||
Public Sub DeleteIniKey(FileName As String, Key As String)
|
||
WriteIni(FileName, Key, Nothing)
|
||
End Sub
|
||
''' <summary>
|
||
''' 写入 ini 文件,这会更新缓存。
|
||
''' 若 Value 为 Nothing,则删除该键。
|
||
''' </summary>
|
||
''' <param name="FileName">文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。</param>
|
||
''' <param name="Key">键。</param>
|
||
''' <param name="Value">值。</param>
|
||
''' <remarks></remarks>
|
||
Public Sub WriteIni(FileName As String, Key As String, Value As String)
|
||
Try
|
||
'预处理
|
||
If Key.Contains(":") Then Throw New Exception($"尝试写入 ini 文件 {FileName} 的键名中包含了冒号:{Key}")
|
||
Key = Key.Replace(vbCr, "").Replace(vbLf, "")
|
||
Value = Value?.Replace(vbCr, "").Replace(vbLf, "")
|
||
'防止争用
|
||
SyncLock WriteIniLock
|
||
'获取目前文件
|
||
Dim Content As SafeDictionary(Of String, String) = IniGetContent(FileName)
|
||
If Content Is Nothing Then Content = New SafeDictionary(Of String, String)
|
||
'更新值
|
||
If Value Is Nothing Then
|
||
If Not Content.ContainsKey(Key) Then Return '无需处理
|
||
Content.Remove(Key)
|
||
Else
|
||
If Content.ContainsKey(Key) AndAlso Content(Key) = Value Then Return '无需处理
|
||
Content(Key) = Value
|
||
End If
|
||
'写入文件
|
||
Dim FileContent As New StringBuilder
|
||
For Each Pair In Content
|
||
FileContent.Append(Pair.Key)
|
||
FileContent.Append(":")
|
||
FileContent.Append(Pair.Value)
|
||
FileContent.Append(vbCrLf)
|
||
Next
|
||
If Not FileName.Contains(":\") Then FileName = $"{ExePath}PCL\{FileName}.ini"
|
||
WriteFile(FileName, FileContent.ToString)
|
||
End SyncLock
|
||
Catch ex As Exception
|
||
Log(ex, $"写入文件失败({FileName} → {Key}:{Value})", LogLevel.Hint)
|
||
End Try
|
||
End Sub
|
||
Private WriteIniLock As New Object
|
||
|
||
'路径处理
|
||
''' <summary>
|
||
''' 从文件路径或者 Url 获取不包含文件名的路径,或获取文件夹的父文件夹路径。
|
||
''' 取决于原路径格式,路径以 / 或 \ 结尾。
|
||
''' 不包含路径将会抛出异常。
|
||
''' </summary>
|
||
Public Function GetPathFromFullPath(FilePath As String) As String
|
||
If Not (FilePath.Contains("\") OrElse FilePath.Contains("/")) Then Throw New Exception("不包含路径:" & FilePath)
|
||
If FilePath.EndsWithF("\") OrElse FilePath.EndsWithF("/") Then
|
||
'是文件夹路径
|
||
Dim IsRight As Boolean = FilePath.EndsWithF("\")
|
||
FilePath = Left(FilePath, Len(FilePath) - 1)
|
||
GetPathFromFullPath = Left(FilePath, FilePath.LastIndexOfAny({"\", "/"})) & If(IsRight, "\", "/")
|
||
Else
|
||
'是文件路径
|
||
GetPathFromFullPath = Left(FilePath, FilePath.LastIndexOfAny({"\", "/"}) + 1)
|
||
If GetPathFromFullPath = "" Then Throw New Exception("不包含路径:" & FilePath)
|
||
End If
|
||
End Function
|
||
''' <summary>
|
||
''' 从文件路径或者 Url 获取不包含路径的文件名。不包含文件名将会抛出异常。
|
||
''' </summary>
|
||
Public Function GetFileNameFromPath(FilePath As String) As String
|
||
FilePath = FilePath.Replace("/", "\")
|
||
If FilePath.EndsWithF("\") Then Throw New Exception("不包含文件名:" & FilePath)
|
||
If FilePath.Contains("?") Then FilePath = FilePath.Substring(0, FilePath.IndexOfF("?")) '去掉网络参数后的 ?
|
||
If FilePath.Contains("\") Then FilePath = FilePath.Substring(FilePath.LastIndexOfF("\") + 1)
|
||
Dim length As Integer = FilePath.Length
|
||
If length = 0 Then Throw New Exception("不包含文件名:" & FilePath)
|
||
If length > 250 Then Throw New PathTooLongException("文件名过长:" & FilePath)
|
||
Return FilePath
|
||
End Function
|
||
''' <summary>
|
||
''' 从文件路径或者 Url 获取不包含路径与扩展名的文件名。不包含文件名将会抛出异常。
|
||
''' </summary>
|
||
Public Function GetFileNameWithoutExtentionFromPath(FilePath As String) As String
|
||
Return IO.Path.GetFileNameWithoutExtension(FilePath)
|
||
End Function
|
||
''' <summary>
|
||
''' 从文件夹路径获取文件夹名。
|
||
''' </summary>
|
||
Public Function GetFolderNameFromPath(FolderPath As String) As String
|
||
If FolderPath.EndsWithF(":\") OrElse FolderPath.EndsWithF(":\\") Then Return FolderPath.Substring(0, 1)
|
||
If FolderPath.EndsWithF("\") OrElse FolderPath.EndsWithF("/") Then FolderPath = Left(FolderPath, FolderPath.Length - 1)
|
||
Return GetFileNameFromPath(FolderPath)
|
||
End Function
|
||
|
||
'读取、写入、复制文件
|
||
''' <summary>
|
||
''' 复制文件。会自动创建文件夹、会覆盖已有的文件。
|
||
''' </summary>
|
||
Public Sub CopyFile(FromPath As String, ToPath As String)
|
||
Try
|
||
'还原文件路径
|
||
If Not FromPath.Contains(":\") Then FromPath = ExePath & FromPath
|
||
If Not ToPath.Contains(":\") Then ToPath = ExePath & ToPath
|
||
'如果复制同一个文件则跳过
|
||
If FromPath = ToPath Then Return
|
||
'确保目录存在
|
||
Directory.CreateDirectory(GetPathFromFullPath(ToPath))
|
||
'复制文件
|
||
File.Copy(FromPath, ToPath, True)
|
||
Catch ex As Exception
|
||
Throw New Exception("复制文件出错:" & FromPath & " → " & ToPath, ex)
|
||
End Try
|
||
End Sub
|
||
''' <summary>
|
||
''' 读取文件,如果失败则返回空数组。
|
||
''' </summary>
|
||
Public Function ReadFileBytes(FilePath As String, Optional Encoding As Encoding = Nothing) As Byte()
|
||
Try
|
||
'还原文件路径
|
||
If Not FilePath.Contains(":\") Then FilePath = ExePath & FilePath
|
||
If File.Exists(FilePath) Then
|
||
Using ReadStream As New FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite) '支持读取使用中的文件
|
||
Using ms As New MemoryStream
|
||
ReadStream.CopyTo(ms)
|
||
Return ms.ToArray()
|
||
End Using
|
||
End Using
|
||
Else
|
||
Log("[System] 欲读取的文件不存在,已返回空内容:" & FilePath)
|
||
Return {}
|
||
End If
|
||
Catch ex As Exception
|
||
Log(ex, "读取文件出错:" & FilePath)
|
||
Return {}
|
||
End Try
|
||
End Function
|
||
''' <summary>
|
||
''' 读取文件,如果失败则返回空字符串。
|
||
''' </summary>
|
||
''' <param name="FilePath">文件完整或相对路径。</param>
|
||
Public Function ReadFile(FilePath As String, Optional Encoding As Encoding = Nothing) As String
|
||
Dim FileBytes = ReadFileBytes(FilePath)
|
||
ReadFile = If(Encoding Is Nothing, DecodeBytes(FileBytes), Encoding.GetString(FileBytes))
|
||
End Function
|
||
''' <summary>
|
||
''' 读取流中的所有文本。
|
||
''' </summary>
|
||
Public Function ReadFile(Stream As Stream, Optional Encoding As Encoding = Nothing) As String
|
||
Try
|
||
Dim readedContent As New MemoryStream()
|
||
Stream.CopyTo(readedContent)
|
||
Dim Bts = readedContent.ToArray()
|
||
Return If(Encoding, EncodingDetector.DetectEncoding(Bts)).GetString(Bts)
|
||
Catch ex As Exception
|
||
Log(ex, "读取流出错")
|
||
Return ""
|
||
End Try
|
||
End Function
|
||
''' <summary>
|
||
''' 写入文件。
|
||
''' </summary>
|
||
''' <param name="FilePath">文件完整或相对路径。</param>
|
||
''' <param name="Text">文件内容。</param>
|
||
''' <param name="Append">是否将文件内容追加到当前文件,而不是覆盖它。</param>
|
||
Public Sub WriteFile(FilePath As String, Text As String, Optional Append As Boolean = False, Optional Encoding As Encoding = Nothing)
|
||
'处理相对路径
|
||
If Not FilePath.Contains(":\") Then FilePath = ExePath & FilePath
|
||
'确保目录存在
|
||
Directory.CreateDirectory(GetPathFromFullPath(FilePath))
|
||
'写入文件
|
||
If Append Then
|
||
'追加目前文件
|
||
Using writer As New StreamWriter(FilePath, True, If(Encoding, EncodingDetector.DetectEncoding(ReadFileBytes(FilePath))))
|
||
writer.Write(Text)
|
||
End Using
|
||
Else
|
||
'直接写入字节
|
||
File.WriteAllBytes(FilePath, If(Encoding Is Nothing, New UTF8Encoding(False).GetBytes(Text), Encoding.GetBytes(Text)))
|
||
End If
|
||
End Sub
|
||
''' <summary>
|
||
''' 写入文件。
|
||
''' 如果 CanThrow 设置为 False,返回是否写入成功。
|
||
''' </summary>
|
||
''' <param name="FilePath">文件完整或相对路径。</param>
|
||
''' <param name="Content">文件内容。</param>
|
||
''' <param name="Append">是否将文件内容追加到当前文件,而不是覆盖它。</param>
|
||
Public Sub WriteFile(FilePath As String, Content As Byte(), Optional Append As Boolean = False)
|
||
'处理相对路径
|
||
If Not FilePath.Contains(":\") Then FilePath = ExePath & FilePath
|
||
'确保目录存在
|
||
Directory.CreateDirectory(GetPathFromFullPath(FilePath))
|
||
'写入文件
|
||
File.WriteAllBytes(FilePath, Content)
|
||
End Sub
|
||
''' <summary>
|
||
''' 将流写入文件。
|
||
''' </summary>
|
||
''' <param name="FilePath">文件完整或相对路径。</param>
|
||
Public Function WriteFile(FilePath As String, Stream As Stream) As Boolean
|
||
Try
|
||
'还原文件路径
|
||
If Not FilePath.Contains(":\") Then FilePath = ExePath & FilePath
|
||
'确保目录存在
|
||
Directory.CreateDirectory(GetPathFromFullPath(FilePath))
|
||
'读取流
|
||
Using fs As New FileStream(FilePath, FileMode.Create, FileAccess.Write)
|
||
fs.SetLength(0)
|
||
Stream.CopyTo(fs)
|
||
End Using
|
||
Return True
|
||
Catch ex As Exception
|
||
Log(ex, "保存流出错")
|
||
Return False
|
||
End Try
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 解码 Bytes。
|
||
''' </summary>
|
||
Public Function DecodeBytes(Bytes As Byte()) As String
|
||
Dim Length As Integer = Bytes.Length
|
||
If Length < 3 Then Return Encoding.UTF8.GetString(Bytes)
|
||
'根据 BOM 判断编码
|
||
If Bytes(0) >= &HEF Then
|
||
'有 BOM 类型
|
||
If Bytes(0) = &HEF AndAlso Bytes(1) = &HBB Then
|
||
Return Encoding.UTF8.GetString(Bytes, 3, Length - 3)
|
||
ElseIf Bytes(0) = &HFE AndAlso Bytes(1) = &HFF Then
|
||
Return Encoding.BigEndianUnicode.GetString(Bytes, 3, Length - 3)
|
||
ElseIf Bytes(0) = &HFF AndAlso Bytes(1) = &HFE Then
|
||
Return Encoding.Unicode.GetString(Bytes, 3, Length - 3)
|
||
Else
|
||
Return Encoding.GetEncoding("GB18030").GetString(Bytes, 3, Length - 3)
|
||
End If
|
||
End If
|
||
'无 BOM 文件:GB18030(ANSI)或 UTF8
|
||
Dim UTF8 = Encoding.UTF8.GetString(Bytes)
|
||
Dim ErrorChar As Char = Encoding.UTF8.GetString({239, 191, 189}).ToCharArray()(0)
|
||
If UTF8.Contains(ErrorChar) Then
|
||
Return Encoding.GetEncoding("GB18030").GetString(Bytes)
|
||
Else
|
||
Return UTF8
|
||
End If
|
||
End Function
|
||
|
||
'文件校验
|
||
''' <summary>
|
||
''' 获取文件 MD5,若失败则返回空字符串。
|
||
''' </summary>
|
||
Public Function GetFileMD5(FilePath As String) As String
|
||
Dim Retry As Boolean = False
|
||
Re:
|
||
Try
|
||
'获取 MD5
|
||
Using fs As New FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
|
||
Return Hash.MD5Provider.Instance.ComputeHash(fs)
|
||
End Using
|
||
Catch ex As Exception
|
||
If Retry OrElse TypeOf ex Is FileNotFoundException Then
|
||
Log(ex, "获取文件 MD5 失败:" & FilePath)
|
||
Return ""
|
||
Else
|
||
Retry = True
|
||
Log(ex, "获取文件 MD5 可重试失败:" & FilePath, LogLevel.Normal)
|
||
Thread.Sleep(RandomUtils.NextInt(200, 500))
|
||
GoTo Re
|
||
End If
|
||
End Try
|
||
End Function
|
||
''' <summary>
|
||
''' 获取文件 SHA512,若失败则返回空字符串。
|
||
''' </summary>
|
||
Public Function GetFileSHA512(FilePath As String) As String
|
||
Dim Retry As Boolean = False
|
||
Re:
|
||
Try
|
||
''检测该文件是否在下载中,若在下载则放弃检测
|
||
'If IgnoreOnDownloading AndAlso NetManage.Files.ContainsKey(FilePath) AndAlso NetManage.Files(FilePath).State <= NetState.Merge Then Return ""
|
||
'获取 SHA512
|
||
Using fs As New FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
|
||
Return Core.Utils.Hash.SHA512Provider.Instance.ComputeHash(fs)
|
||
End Using
|
||
Catch ex As Exception
|
||
If Retry OrElse TypeOf ex Is FileNotFoundException Then
|
||
Log(ex, "获取文件 SHA512 失败:" & FilePath)
|
||
Return ""
|
||
Else
|
||
Retry = True
|
||
Log(ex, "获取文件 SHA512 可重试失败:" & FilePath, LogLevel.Normal)
|
||
Thread.Sleep(RandomUtils.NextInt(200, 500))
|
||
GoTo Re
|
||
End If
|
||
End Try
|
||
End Function
|
||
''' <summary>
|
||
''' 获取文件 SHA256,若失败则返回空字符串。
|
||
''' </summary>
|
||
Public Function GetFileSHA256(FilePath As String) As String
|
||
Dim Retry As Boolean = False
|
||
Re:
|
||
Try
|
||
''检测该文件是否在下载中,若在下载则放弃检测
|
||
'If IgnoreOnDownloading AndAlso NetManage.Files.ContainsKey(FilePath) AndAlso NetManage.Files(FilePath).State <= NetState.Merge Then Return ""
|
||
'获取 SHA256
|
||
Using fs As New FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
|
||
Return Core.Utils.Hash.SHA256Provider.Instance.ComputeHash(fs)
|
||
End Using
|
||
Catch ex As Exception
|
||
If Retry OrElse TypeOf ex Is FileNotFoundException Then
|
||
Log(ex, "获取文件 SHA256 失败:" & FilePath)
|
||
Return ""
|
||
Else
|
||
Retry = True
|
||
Log(ex, "获取文件 SHA256 可重试失败:" & FilePath, LogLevel.Normal)
|
||
Thread.Sleep(RandomUtils.NextInt(200, 500))
|
||
GoTo Re
|
||
End If
|
||
End Try
|
||
End Function
|
||
''' <summary>
|
||
''' 获取文件 SHA1,若失败则返回空字符串。
|
||
''' </summary>
|
||
Public Function GetFileSHA1(FilePath As String) As String
|
||
Dim Retry As Boolean = False
|
||
Re:
|
||
Try
|
||
'获取 SHA1
|
||
Using fs As New FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
|
||
Return Core.Utils.Hash.SHA1Provider.Instance.ComputeHash(fs)
|
||
End Using
|
||
Catch ex As Exception
|
||
If Retry OrElse TypeOf ex Is FileNotFoundException Then
|
||
Log(ex, "获取文件 SHA1 失败:" & FilePath)
|
||
Return ""
|
||
Else
|
||
Retry = True
|
||
Log(ex, "获取文件 SHA1 可重试失败:" & FilePath, LogLevel.Normal)
|
||
Thread.Sleep(RandomUtils.NextInt(200, 500))
|
||
GoTo Re
|
||
End If
|
||
End Try
|
||
End Function
|
||
''' <summary>
|
||
''' 获取流的 SHA1,若失败则返回空字符串。
|
||
''' </summary>
|
||
Public Function GetAuthSHA1(inputStream As Stream) As String
|
||
Try
|
||
Return Core.Utils.Hash.SHA1Provider.Instance.ComputeHash(inputStream)
|
||
Catch ex As Exception
|
||
Log(ex, "获取流 SHA1 失败")
|
||
Return ""
|
||
End Try
|
||
End Function
|
||
''' <summary>
|
||
''' 文件的校验规则。
|
||
''' </summary>
|
||
Public Class FileChecker
|
||
''' <summary>
|
||
''' 文件的准确大小。
|
||
''' </summary>
|
||
Public ActualSize As Long = -1
|
||
''' <summary>
|
||
''' 文件的最小大小。
|
||
''' </summary>
|
||
Public MinSize As Long = -1
|
||
''' <summary>
|
||
''' 文件的 MD5、SHA1 或 SHA256。会根据输入字符串的长度自动判断种类。
|
||
''' </summary>
|
||
Public Hash As String = Nothing
|
||
''' <summary>
|
||
''' 是否可以使用已经存在的文件。
|
||
''' </summary>
|
||
Public CanUseExistsFile As Boolean = True
|
||
''' <summary>
|
||
''' 是否为 Json 文件。
|
||
''' </summary>
|
||
Public IsJson As Boolean = False
|
||
Public Sub New(Optional MinSize As Long = -1, Optional ActualSize As Long = -1, Optional Hash As String = Nothing, Optional CanUseExistsFile As Boolean = True, Optional IsJson As Boolean = False)
|
||
Me.ActualSize = ActualSize
|
||
Me.MinSize = MinSize
|
||
Me.Hash = Hash
|
||
Me.CanUseExistsFile = CanUseExistsFile
|
||
Me.IsJson = IsJson
|
||
End Sub
|
||
''' <summary>
|
||
''' 检查文件。若成功则返回 Nothing,失败则返回错误的描述文本,描述文本不以句号结尾。不会抛出错误。
|
||
''' </summary>
|
||
Public Function Check(LocalPath As String) As String
|
||
Try
|
||
Log($"[Checker] 开始校验文件 {LocalPath}", LogLevel.Developer)
|
||
Dim Info As New FileInfo(LocalPath)
|
||
If Not Info.Exists Then Return "文件不存在:" & LocalPath
|
||
Dim FileSize As Long = Info.Length
|
||
Dim ErrorMessage As New List(Of String)
|
||
Dim AllowIgnore As Boolean = False '允许相信哈希正确但是大小不正确
|
||
If Not String.IsNullOrEmpty(Hash) Then
|
||
If Hash.Length < 35 Then 'MD5
|
||
Dim ComputedHash As String = GetFileMD5(LocalPath)
|
||
If Hash.ToLowerInvariant <> ComputedHash Then
|
||
ErrorMessage.Add("文件 MD5 应为 " & Hash & ",实际为 " & ComputedHash)
|
||
End If
|
||
ElseIf Hash.Length = 64 Then 'SHA256
|
||
Dim ComputedHash As String = GetFileSHA256(LocalPath)
|
||
If Hash.ToLowerInvariant <> ComputedHash Then
|
||
ErrorMessage.Add("文件 SHA256 应为 " & Hash & ",实际为 " & ComputedHash)
|
||
End If
|
||
Else 'SHA1 (40)
|
||
Dim ComputedHash As String = GetFileSHA1(LocalPath)
|
||
If Hash.ToLowerInvariant <> ComputedHash Then
|
||
ErrorMessage.Add("文件 SHA1 应为 " & Hash & ",实际为 " & ComputedHash)
|
||
End If
|
||
End If
|
||
AllowIgnore = ErrorMessage.Count = 0
|
||
End If
|
||
|
||
If ActualSize >= 0 AndAlso ActualSize <> FileSize AndAlso Not AllowIgnore Then '不允许忽略大小不正确的情况
|
||
ErrorMessage.Add($"文件大小应为 {ActualSize} B,实际为 {FileSize} B" &
|
||
If(FileSize < 2000, ",内容为" & ReadFile(LocalPath), ""))
|
||
End If
|
||
|
||
If MinSize >= 0 AndAlso MinSize > FileSize Then
|
||
ErrorMessage.Add($"文件大小应大于 {MinSize} B,实际为 {FileSize} B" &
|
||
If(FileSize < 2000, ",内容为:" & ReadFile(LocalPath), ""))
|
||
End If
|
||
|
||
If IsJson Then
|
||
Dim Content As String = ReadFile(LocalPath)
|
||
If Content = "" Then Throw New Exception("读取到的文件为空")
|
||
Try
|
||
GetJson(Content)
|
||
Catch ex As Exception
|
||
Throw New Exception("不是有效的 Json 文件", ex)
|
||
End Try
|
||
End If
|
||
If ErrorMessage.Count <> 0 Then
|
||
ErrorMessage.Insert(0, $"实际校验地址:{LocalPath}")
|
||
Return ErrorMessage.Join(";")
|
||
End If
|
||
Return Nothing
|
||
Catch ex As Exception
|
||
Log(ex, "检查文件出错")
|
||
Return ex.ToString()
|
||
End Try
|
||
End Function
|
||
End Class
|
||
|
||
''' <summary>
|
||
''' 尝试根据后缀名判断文件种类并解压文件,支持 gz 与 zip,会尝试将 Jar 以 zip 方式解压。
|
||
''' 会尝试创建,但不会清空目标文件夹。
|
||
''' </summary>
|
||
Public Sub ExtractFile(CompressFilePath As String, DestDirectory As String, Optional Encode As Encoding = Nothing,
|
||
Optional ProgressIncrementHandler As Action(Of Double) = Nothing)
|
||
Directory.CreateDirectory(DestDirectory)
|
||
If CompressFilePath.EndsWithF(".gz", True) Then
|
||
'以 gz 方式解压
|
||
Using compressedFile As New FileStream(CompressFilePath, FileMode.Open, FileAccess.Read)
|
||
Using decompressStream As New GZipStream(compressedFile, CompressionMode.Decompress)
|
||
Using extractFileStream As New FileStream(IO.Path.Combine(DestDirectory, GetFileNameFromPath(CompressFilePath).ToLower.Replace(".tar", "").Replace(".gz", "")), FileMode.OpenOrCreate, FileAccess.Write)
|
||
decompressStream.CopyTo(extractFileStream)
|
||
End Using
|
||
End Using
|
||
End Using
|
||
Else
|
||
'以 zip 方式解压
|
||
Using Archive = ZipFile.Open(CompressFilePath, ZipArchiveMode.Read, If(Encode, Encoding.GetEncoding("GB18030")))
|
||
Dim TotalCount As Integer = Archive.Entries.Count
|
||
For Each Entry As ZipArchiveEntry In Archive.Entries
|
||
If ProgressIncrementHandler IsNot Nothing Then ProgressIncrementHandler(1 / TotalCount)
|
||
Dim DestinationPath As String = IO.Path.Combine(DestDirectory, Entry.FullName)
|
||
If DestinationPath.EndsWithF("\") OrElse DestinationPath.EndsWithF("/") Then
|
||
Continue For '不创建空文件夹
|
||
Else
|
||
Directory.CreateDirectory(GetPathFromFullPath(DestinationPath))
|
||
Entry.ExtractToFile(DestinationPath, True)
|
||
End If
|
||
Next
|
||
End Using
|
||
End If
|
||
End Sub
|
||
|
||
''' <summary>
|
||
''' 删除文件夹,返回删除的文件个数。通过参数选择是否抛出异常。
|
||
''' </summary>
|
||
Public Function DeleteDirectory(Path As String, Optional IgnoreIssue As Boolean = False) As Integer
|
||
If Not Directory.Exists(Path) Then Return 0
|
||
Dim DeletedCount As Integer = 0
|
||
Dim Files As String()
|
||
Try
|
||
Files = Directory.GetFiles(Path)
|
||
Catch ex As DirectoryNotFoundException '#4549
|
||
Log(ex, $"疑似为孤立符号链接,尝试直接删除({Path})", LogLevel.Developer)
|
||
Directory.Delete(Path)
|
||
Return 0
|
||
End Try
|
||
For Each FilePath As String In Files
|
||
Dim RetriedFile As Boolean = False
|
||
RetryFile:
|
||
Try
|
||
File.Delete(FilePath)
|
||
DeletedCount += 1
|
||
Catch ex As Exception
|
||
If Not RetriedFile Then
|
||
RetriedFile = True
|
||
Log(ex, $"删除文件失败,将在 0.3s 后重试({FilePath})")
|
||
Thread.Sleep(300)
|
||
GoTo RetryFile
|
||
ElseIf IgnoreIssue Then
|
||
Log(ex, "删除单个文件可忽略地失败")
|
||
Else
|
||
Throw
|
||
End If
|
||
End Try
|
||
Next
|
||
For Each str As String In Directory.GetDirectories(Path)
|
||
DeleteDirectory(str, IgnoreIssue)
|
||
Next
|
||
Dim RetriedDir As Boolean = False
|
||
RetryDir:
|
||
Try
|
||
Directory.Delete(Path, True)
|
||
Catch ex As Exception
|
||
If Not RetriedDir AndAlso Not RunInUi() Then
|
||
RetriedDir = True
|
||
Log(ex, $"删除文件夹失败,将在 0.3s 后重试({Path})")
|
||
Thread.Sleep(300)
|
||
GoTo RetryDir
|
||
ElseIf IgnoreIssue Then
|
||
Log(ex, "删除单个文件夹可忽略地失败")
|
||
Else
|
||
Throw
|
||
End If
|
||
End Try
|
||
Return DeletedCount
|
||
End Function
|
||
''' <summary>
|
||
''' 复制文件夹,失败会抛出异常。
|
||
''' </summary>
|
||
Public Sub CopyDirectory(FromPath As String, ToPath As String, Optional ProgressIncrementHandler As Action(Of Double) = Nothing)
|
||
FromPath = FromPath.Replace("/", "\")
|
||
If Not FromPath.EndsWithF("\") Then FromPath &= "\"
|
||
ToPath = ToPath.Replace("/", "\")
|
||
If Not ToPath.EndsWithF("\") Then ToPath &= "\"
|
||
Dim AllFiles = EnumerateFiles(FromPath).ToList
|
||
Dim FileCount As Integer = AllFiles.Count
|
||
For Each File In AllFiles
|
||
CopyFile(File.FullName, File.FullName.Replace(FromPath, ToPath))
|
||
If ProgressIncrementHandler IsNot Nothing Then ProgressIncrementHandler(1 / FileCount)
|
||
Next
|
||
End Sub
|
||
''' <summary>
|
||
''' 遍历文件夹中的所有文件。
|
||
''' </summary>
|
||
Public Function EnumerateFiles(Directory As String) As IEnumerable(Of FileInfo)
|
||
Dim Info As New DirectoryInfo(ShortenPath(Directory))
|
||
If Not Info.Exists Then Return New List(Of FileInfo)
|
||
Return Info.EnumerateFiles("*", SearchOption.AllDirectories)
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 若路径长度大于指定值,则将长路径转换为短路径。
|
||
''' </summary>
|
||
Public Function ShortenPath(LongPath As String, Optional ShortenThreshold As Integer = 247) As String
|
||
If LongPath.Length <= ShortenThreshold Then Return LongPath
|
||
Dim ShortPath As New StringBuilder(260)
|
||
GetShortPathName(LongPath, ShortPath, 260)
|
||
Return ShortPath.ToString
|
||
End Function
|
||
|
||
Public Sub MoveDirectory(SourceDir As String, TargetDir As String)
|
||
If Not Directory.Exists(TargetDir) Then Directory.CreateDirectory(TargetDir)
|
||
For Each FilePath In Directory.GetFiles(SourceDir)
|
||
Dim FileName = GetFileNameFromPath(FilePath)
|
||
File.Move(FilePath, IO.Path.Combine(TargetDir, FileName))
|
||
Next
|
||
For Each DirPath In Directory.GetDirectories(SourceDir)
|
||
Dim DirName = GetFolderNameFromPath(DirPath)
|
||
MoveDirectory(DirPath, IO.Path.Combine(TargetDir, DirName))
|
||
Next
|
||
End Sub
|
||
Private Declare Function GetShortPathName Lib "kernel32" Alias "GetShortPathNameA" (ByVal lpszLongPath As String, ByVal lpszShortPath As StringBuilder, ByVal cchBuffer As Integer) As Integer
|
||
|
||
Public Sub CreateSymbolicLink(ByVal LinkPath As String, ByVal TargetPath As String, ByVal Flags As Integer)
|
||
Dim CMDProcess As New Process
|
||
Dim LinkDPath = ExtractLinkD()
|
||
With CMDProcess.StartInfo
|
||
.FileName = LinkDPath
|
||
.Arguments = $"""{LinkPath}"" ""{TargetPath}"""
|
||
.CreateNoWindow = True
|
||
.UseShellExecute = False
|
||
End With
|
||
CMDProcess.Start()
|
||
While Not CMDProcess.HasExited
|
||
End While
|
||
End Sub
|
||
#End Region
|
||
|
||
#Region "文本"
|
||
Public vbLQ As Char = Convert.ToChar(8220)
|
||
Public vbRQ As Char = Convert.ToChar(8221)
|
||
|
||
''' <summary>
|
||
''' 返回一个枚举对应的字符串。
|
||
''' </summary>
|
||
''' <param name="EnumData">一个已经实例化的枚举类型。</param>
|
||
Public Function GetStringFromEnum(EnumData As [Enum]) As String
|
||
Return [Enum].GetName(EnumData.GetType, EnumData)
|
||
End Function
|
||
''' <summary>
|
||
''' 将文件大小转化为适合的文本形式,如“1.28 M”。
|
||
''' </summary>
|
||
''' <param name="FileSize">以字节为单位的大小表示。</param>
|
||
Public Function GetString(FileSize As Long) As String
|
||
Dim IsNegative = FileSize < 0
|
||
If IsNegative Then FileSize *= -1
|
||
If FileSize < 1000 Then
|
||
'B 级
|
||
Return If(IsNegative, "-", "") & FileSize & " B"
|
||
ElseIf FileSize < 1024 * 1000 Then
|
||
'K 级
|
||
Dim RoundResult As String = Math.Round(FileSize / 1024)
|
||
Return If(IsNegative, "-", "") & Math.Round(FileSize / 1024, CInt(MathClamp(3 - RoundResult.Length, 0, 2))) & " K"
|
||
ElseIf FileSize < 1024 * 1024 * 1000 Then
|
||
'M 级
|
||
Dim RoundResult As String = Math.Round(FileSize / 1024 / 1024)
|
||
Return If(IsNegative, "-", "") & Math.Round(FileSize / 1024 / 1024, CInt(MathClamp(3 - RoundResult.Length, 0, 2))) & " M"
|
||
Else
|
||
'G 级
|
||
Dim RoundResult As String = Math.Round(FileSize / 1024 / 1024 / 1024)
|
||
Return If(IsNegative, "-", "") & Math.Round(FileSize / 1024 / 1024 / 1024, CInt(MathClamp(3 - RoundResult.Length, 0, 2))) & " G"
|
||
End If
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 获取 JSON 对象。
|
||
''' </summary>
|
||
Public Function GetJson(Data As String)
|
||
Try
|
||
Return JsonConvert.DeserializeObject(Data, New JsonSerializerSettings With {.DateTimeZoneHandling = DateTimeZoneHandling.Local})
|
||
Catch ex As Exception
|
||
Dim Length As Integer = If(Data, "").Length
|
||
Throw New Exception("格式化 JSON 失败:" & If(Length > 2000, Data.Substring(0, 500) & $"...(全长 {Length} 个字符)..." & Right(Data, 500), Data))
|
||
End Try
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 将第一个字符转换为大写,其余字符转换为小写。
|
||
''' </summary>
|
||
<Extension> Public Function Capitalize(word As String) As String
|
||
If String.IsNullOrEmpty(word) Then Return word
|
||
Return word.Substring(0, 1).ToUpperInvariant() & word.Substring(1).ToLowerInvariant()
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 将字符串统一至某个长度,过短则以 Code 将其右侧填充,过长则截取靠左的指定长度。
|
||
''' </summary>
|
||
Public Function StrFill(Str As String, Code As String, Length As Byte) As String
|
||
If Str.Length > Length Then Return Mid(Str, 1, Length)
|
||
Return Mid(Str.PadRight(Length, Code), Str.Length + 1) & Str
|
||
End Function
|
||
''' <summary>
|
||
''' 将一个小数显示为固定的小数点后位数形式,将向零取整。
|
||
''' 如 12 保留 2 位则输出 12.00,而 95.678 保留 2 位则输出 95.67。
|
||
''' </summary>
|
||
Public Function StrFillNum(Num As Double, Length As Integer) As String
|
||
Num = Math.Round(Num, Length, MidpointRounding.AwayFromZero)
|
||
StrFillNum = Num
|
||
If Not StrFillNum.Contains(".") Then Return (StrFillNum & ".").PadRight(StrFillNum.Length + 1 + Length, "0")
|
||
Return StrFillNum.PadRight(StrFillNum.Split(".")(0).Length + 1 + Length, "0")
|
||
End Function
|
||
''' <summary>
|
||
''' 移除字符串首尾的标点符号、回车,以及括号中、冒号后的补充说明内容。
|
||
''' </summary>
|
||
Public Function StrTrim(Str As String, Optional RemoveQuote As Boolean = True)
|
||
If RemoveQuote Then Str = Str.Split("(")(0).Split(":")(0).Split("(")(0).Split(":")(0)
|
||
Return Str.Trim(".", "。", "!", " ", "!", "?", "?", vbCr, vbLf)
|
||
End Function
|
||
''' <summary>
|
||
''' 连接字符串。
|
||
''' </summary>
|
||
<Extension> Public Function Join(List As IEnumerable, Split As String) As String
|
||
Dim Builder As New StringBuilder
|
||
Dim IsFirst As Boolean = True
|
||
For Each Element In List
|
||
If IsFirst Then
|
||
IsFirst = False
|
||
Else
|
||
Builder.Append(Split)
|
||
End If
|
||
If Element IsNot Nothing Then Builder.Append(Element)
|
||
Next
|
||
Return Builder.ToString
|
||
End Function
|
||
''' <summary>
|
||
''' 分割字符串。
|
||
''' </summary>
|
||
<Extension> Public Function Split(FullStr As String, SplitStr As String) As String()
|
||
If SplitStr.Length = 1 Then
|
||
Return FullStr.Split(SplitStr(0))
|
||
Else
|
||
Return FullStr.Split({SplitStr}, StringSplitOptions.None)
|
||
End If
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 获取字符串哈希值。
|
||
''' </summary>
|
||
Public Function GetHash(Str As String) As ULong
|
||
GetHash = 5381
|
||
For i = 0 To Str.Length - 1
|
||
GetHash = (GetHash << 5) Xor GetHash Xor CType(AscW(Str(i)), ULong)
|
||
Next
|
||
Return GetHash Xor &HA98F501BC684032FUL
|
||
End Function
|
||
''' <summary>
|
||
''' 获取字符串 MD5。
|
||
''' </summary>
|
||
Public Function GetStringMD5(Str As String) As String
|
||
Return Core.Utils.Hash.MD5Provider.Instance.ComputeHash(Str)
|
||
End Function
|
||
''' <summary>
|
||
''' 检查字符串中的字符是否均为 ASCII 字符。
|
||
''' </summary>
|
||
<Extension> Public Function IsASCII(Input As String) As Boolean
|
||
Return Input.All(Function(c) AscW(c) < 128)
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 获取在子字符串第一次出现之前的部分,例如对 2024/11/08 拆切 / 会得到 2024。
|
||
''' 如果未找到子字符串则不裁切。
|
||
''' </summary>
|
||
<Extension> Public Function BeforeFirst(Str As String, Text As String, Optional IgnoreCase As Boolean = False) As String
|
||
Dim Pos As Integer = If(String.IsNullOrEmpty(Text), -1, Str.IndexOfF(Text, IgnoreCase))
|
||
If Pos >= 0 Then
|
||
Return Str.Substring(0, Pos)
|
||
Else
|
||
Return Str
|
||
End If
|
||
End Function
|
||
''' <summary>
|
||
''' 获取在子字符串最后一次出现之前的部分,例如对 2024/11/08 拆切 / 会得到 2024/11。
|
||
''' 如果未找到子字符串则不裁切。
|
||
''' </summary>
|
||
<Extension> Public Function BeforeLast(Str As String, Text As String, Optional IgnoreCase As Boolean = False) As String
|
||
Dim Pos As Integer = If(String.IsNullOrEmpty(Text), -1, Str.LastIndexOfF(Text, IgnoreCase))
|
||
If Pos >= 0 Then
|
||
Return Str.Substring(0, Pos)
|
||
Else
|
||
Return Str
|
||
End If
|
||
End Function
|
||
''' <summary>
|
||
''' 获取在子字符串第一次出现之后的部分,例如对 2024/11/08 拆切 / 会得到 11/08。
|
||
''' 如果未找到子字符串则不裁切。
|
||
''' </summary>
|
||
<Extension> Public Function AfterFirst(Str As String, Text As String, Optional IgnoreCase As Boolean = False) As String
|
||
Dim Pos As Integer = If(String.IsNullOrEmpty(Text), -1, Str.IndexOfF(Text, IgnoreCase))
|
||
If Pos >= 0 Then
|
||
Return Str.Substring(Pos + Text.Length)
|
||
Else
|
||
Return Str
|
||
End If
|
||
End Function
|
||
''' <summary>
|
||
''' 获取在子字符串最后一次出现之后的部分,例如对 2024/11/08 拆切 / 会得到 08。
|
||
''' 如果未找到子字符串则不裁切。
|
||
''' </summary>
|
||
<Extension> Public Function AfterLast(Str As String, Text As String, Optional IgnoreCase As Boolean = False) As String
|
||
Dim Pos As Integer = If(String.IsNullOrEmpty(Text), -1, Str.LastIndexOfF(Text, IgnoreCase))
|
||
If Pos >= 0 Then
|
||
Return Str.Substring(Pos + Text.Length)
|
||
Else
|
||
Return Str
|
||
End If
|
||
End Function
|
||
''' <summary>
|
||
''' 获取处于两个子字符串之间的部分,裁切尽可能多的内容。
|
||
''' 等效于 AfterLast 后接 BeforeFirst。
|
||
''' 如果未找到子字符串则不裁切。
|
||
''' </summary>
|
||
<Extension> Public Function Between(Str As String, After As String, Before As String, Optional IgnoreCase As Boolean = False) As String
|
||
Dim StartPos As Integer = If(String.IsNullOrEmpty(After), -1, Str.LastIndexOfF(After, IgnoreCase))
|
||
If StartPos >= 0 Then
|
||
StartPos += After.Length
|
||
Else
|
||
StartPos = 0
|
||
End If
|
||
Dim EndPos As Integer = If(String.IsNullOrEmpty(Before), -1, Str.IndexOfF(Before, StartPos, IgnoreCase))
|
||
If EndPos >= 0 Then
|
||
Return Str.Substring(StartPos, EndPos - StartPos)
|
||
ElseIf StartPos > 0 Then
|
||
Return Str.Substring(StartPos)
|
||
Else
|
||
Return Str
|
||
End If
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 高速的 StartsWith。
|
||
''' </summary>
|
||
<Extension> <MethodImpl(MethodImplOptions.AggressiveInlining)>
|
||
Public Function StartsWithF(Str As String, Prefix As String, Optional IgnoreCase As Boolean = False) As Boolean
|
||
Return Str.StartsWith(Prefix, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal))
|
||
End Function
|
||
''' <summary>
|
||
''' 高速的 EndsWith。
|
||
''' </summary>
|
||
<Extension> <MethodImpl(MethodImplOptions.AggressiveInlining)>
|
||
Public Function EndsWithF(Str As String, Suffix As String, Optional IgnoreCase As Boolean = False) As Boolean
|
||
Return Str.EndsWith(Suffix, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal))
|
||
End Function
|
||
''' <summary>
|
||
''' 支持可变大小写判断的 Contains。
|
||
''' </summary>
|
||
<Extension> <MethodImpl(MethodImplOptions.AggressiveInlining)>
|
||
Public Function ContainsF(Str As String, SubStr As String, Optional IgnoreCase As Boolean = False) As Boolean
|
||
Return Str.IndexOf(SubStr, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal)) >= 0
|
||
End Function
|
||
''' <summary>
|
||
''' 高速的 IndexOf。
|
||
''' </summary>
|
||
<Extension> <MethodImpl(MethodImplOptions.AggressiveInlining)>
|
||
Public Function IndexOfF(Str As String, SubStr As String, Optional IgnoreCase As Boolean = False) As Integer
|
||
Return Str.IndexOf(SubStr, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal))
|
||
End Function
|
||
''' <summary>
|
||
''' 高速的 IndexOf。
|
||
''' </summary>
|
||
<Extension> <MethodImpl(MethodImplOptions.AggressiveInlining)>
|
||
Public Function IndexOfF(Str As String, SubStr As String, StartIndex As Integer, Optional IgnoreCase As Boolean = False) As Integer
|
||
Return Str.IndexOf(SubStr, StartIndex, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal))
|
||
End Function
|
||
''' <summary>
|
||
''' 高速的 LastIndexOf。
|
||
''' </summary>
|
||
<Extension> <MethodImpl(MethodImplOptions.AggressiveInlining)>
|
||
Public Function LastIndexOfF(Str As String, SubStr As String, Optional IgnoreCase As Boolean = False) As Integer
|
||
Return Str.LastIndexOf(SubStr, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal))
|
||
End Function
|
||
''' <summary>
|
||
''' 高速的 LastIndexOf。
|
||
''' </summary>
|
||
<Extension> <MethodImpl(MethodImplOptions.AggressiveInlining)>
|
||
Public Function LastIndexOfF(Str As String, SubStr As String, StartIndex As Integer, Optional IgnoreCase As Boolean = False) As Integer
|
||
Return Str.LastIndexOf(SubStr, StartIndex, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal))
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 不会报错的 Val。
|
||
''' 如果输入有误,返回 0。
|
||
''' </summary>
|
||
Public Function Val(Str As Object) As Double
|
||
Try
|
||
Return If(TypeOf Str Is String AndAlso Str = "&", 0, Conversion.Val(Str))
|
||
Catch
|
||
Return 0
|
||
End Try
|
||
End Function
|
||
|
||
'转义
|
||
''' <summary>
|
||
''' 为字符串进行 XML 转义。
|
||
''' </summary>
|
||
Public Function EscapeXML(Str As String) As String
|
||
If Str.StartsWithF("{") Then Str = "{}" & Str '#4187
|
||
Return Str.
|
||
Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("'", "'").
|
||
Replace("""", """).Replace(vbCrLf, "
")
|
||
End Function
|
||
''' <summary>
|
||
''' 为字符串进行 Like 关键字转义。
|
||
''' </summary>
|
||
Public Function EscapeLikePattern(input As String) As String
|
||
Dim sb As New StringBuilder()
|
||
For Each c As Char In input
|
||
Select Case c
|
||
Case "["c, "]"c, "*"c, "?"c, "#"c
|
||
sb.Append("["c).Append(c).Append("]"c)
|
||
Case Else
|
||
sb.Append(c)
|
||
End Select
|
||
Next
|
||
Return sb.ToString()
|
||
End Function
|
||
|
||
'正则
|
||
''' <summary>
|
||
''' 搜索字符串中的所有正则匹配项。
|
||
''' </summary>
|
||
<Extension> Public Function RegexSearch(str As String, regex As String, Optional options As RegexOptions = RegexOptions.None) As List(Of String)
|
||
Try
|
||
RegexSearch = New List(Of String)
|
||
Dim RegexSearchRes = New Regex(regex, options).Matches(str)
|
||
If RegexSearchRes Is Nothing Then Return RegexSearch
|
||
For Each item As Match In RegexSearchRes
|
||
RegexSearch.Add(item.Value)
|
||
Next
|
||
Catch ex As Exception
|
||
Log(ex, "正则匹配全部项出错")
|
||
Return New List(Of String)
|
||
End Try
|
||
End Function
|
||
''' <summary>
|
||
''' 获取字符串中的第一个正则匹配项,若无匹配则返回 Nothing。
|
||
''' </summary>
|
||
<Extension> Public Function RegexSeek(str As String, regex As String, Optional options As RegexOptions = RegexOptions.None) As String
|
||
Try
|
||
Dim Result = RegularExpressions.Regex.Match(str, regex, options).Value
|
||
Return If(Result = "", Nothing, Result)
|
||
Catch ex As Exception
|
||
Log(ex, "正则匹配第一项出错")
|
||
Return Nothing
|
||
End Try
|
||
End Function
|
||
''' <summary>
|
||
''' 检查字符串是否匹配某正则模式。
|
||
''' </summary>
|
||
<Extension> Public Function RegexCheck(str As String, regex As String, Optional options As RegexOptions = RegexOptions.None) As Boolean
|
||
Try
|
||
Return RegularExpressions.Regex.IsMatch(str, regex, options)
|
||
Catch ex As Exception
|
||
Log(ex, "正则检查出错")
|
||
Return False
|
||
End Try
|
||
End Function
|
||
''' <summary>
|
||
''' 进行正则替换,会抛出错误。
|
||
''' </summary>
|
||
<Extension> Public Function RegexReplace(AllContents As String, SearchRegex As String, ReplaceTo As String, Optional options As RegexOptions = RegexOptions.None) As String
|
||
Return Regex.Replace(AllContents, SearchRegex, ReplaceTo, options)
|
||
End Function
|
||
''' <summary>
|
||
''' 对每个正则匹配分别进行替换,会抛出错误。
|
||
''' </summary>
|
||
<Extension> Public Function RegexReplaceEach(AllContents As String, SearchRegex As String, ReplaceTo As MatchEvaluator, Optional options As RegexOptions = RegexOptions.None) As String
|
||
Return Regex.Replace(AllContents, SearchRegex, ReplaceTo, options)
|
||
End Function
|
||
|
||
#End Region
|
||
|
||
#Region "搜索"
|
||
|
||
''' <summary>
|
||
''' 获取搜索文本的相似度。
|
||
''' </summary>
|
||
''' <param name="Source">被搜索的长内容。</param>
|
||
''' <param name="Query">用户输入的搜索文本。</param>
|
||
Private Function SearchSimilarity(Source As String, Query As String) As Double
|
||
Dim qp As Integer = 0, lenSum As Double = 0
|
||
Source = Source.ToLower.Replace(" ", "")
|
||
Query = Query.ToLower.Replace(" ", "")
|
||
Dim sourceLength As Integer = Source.Length, queryLength As Integer = Query.Length '用于计算最后因数的长度缓存
|
||
Do While qp < queryLength
|
||
'对 qp 作为开始位置计算
|
||
Dim sp As Integer = 0, lenMax As Integer = 0, spMax As Integer = 0
|
||
'查找以 qp 为头的最大子串
|
||
Do While sp < Source.Length
|
||
'对每个 sp 作为开始位置计算最大子串
|
||
Dim len As Integer = 0
|
||
Do While (qp + len) < queryLength AndAlso (sp + len) < Source.Length AndAlso Source(sp + len) = Query(qp + len)
|
||
len += 1
|
||
Loop
|
||
'存储 len
|
||
If len > lenMax Then
|
||
lenMax = len
|
||
spMax = sp
|
||
End If
|
||
'根据结果增加 sp
|
||
sp += Math.Max(1, len)
|
||
Loop
|
||
If lenMax > 0 Then
|
||
Source = Source.Substring(0, spMax) & If(Source.Count > spMax + lenMax, Source.Substring(spMax + lenMax), String.Empty) '将源中的对应字段替换空
|
||
'存储 lenSum
|
||
Dim IncWeight = (Math.Pow(1.4, 3 + lenMax) - 3.6) '根据长度加成
|
||
IncWeight *= 1 + 0.3 * Math.Max(0, 3 - Math.Abs(qp - spMax)) '根据位置加成
|
||
lenSum += IncWeight
|
||
End If
|
||
'根据结果增加 qp
|
||
qp += Math.Max(1, lenMax)
|
||
Loop
|
||
'计算结果:重复字段量 × 源长度影响比例
|
||
Return (lenSum / queryLength) * (3 / Math.Pow(sourceLength + 15, 0.5)) * If(queryLength <= 2, 3 - queryLength, 1)
|
||
End Function
|
||
''' <summary>
|
||
''' 获取多段文本加权后的相似度。
|
||
''' </summary>
|
||
Private Function SearchSimilarityWeighted(Source As List(Of KeyValuePair(Of String, Double)), Query As String) As Double
|
||
Dim TotalWeight As Double = 0
|
||
Dim Sum As Double = 0
|
||
For Each Pair In Source
|
||
Sum += SearchSimilarity(Pair.Key, Query) * Pair.Value
|
||
TotalWeight += Pair.Value
|
||
Next
|
||
Return Sum / TotalWeight
|
||
End Function
|
||
''' <summary>
|
||
''' 用于搜索的项目。
|
||
''' </summary>
|
||
Public Class SearchEntry(Of T)
|
||
''' <summary>
|
||
''' 该项目对应的源数据。
|
||
''' </summary>
|
||
Public Item As T
|
||
''' <summary>
|
||
''' 该项目用于搜索的源。
|
||
''' </summary>
|
||
Public SearchSource As List(Of KeyValuePair(Of String, Double))
|
||
''' <summary>
|
||
''' 相似度。
|
||
''' </summary>
|
||
Public Similarity As Double
|
||
''' <summary>
|
||
''' 是否完全匹配。
|
||
''' </summary>
|
||
Public AbsoluteRight As Boolean
|
||
End Class
|
||
''' <summary>
|
||
''' 进行多段文本加权搜索,获取相似度较高的数项结果。
|
||
''' </summary>
|
||
''' <param name="MaxBlurCount">返回的最大模糊结果数。</param>
|
||
''' <param name="MinBlurSimilarity">返回结果要求的最低相似度。</param>
|
||
Public Function Search(Of T)(Entries As List(Of SearchEntry(Of T)), Query As String, Optional MaxBlurCount As Integer = 5, Optional MinBlurSimilarity As Double = 0.1) As List(Of SearchEntry(Of T))
|
||
Dim ResultList As New List(Of SearchEntry(Of T))
|
||
|
||
If Entries Is Nothing OrElse Not Entries.Any() Then
|
||
Return ResultList
|
||
End If
|
||
|
||
' Preprocess query into parts
|
||
Dim queryParts As String() = Query.Split(New Char() {" "c}, StringSplitOptions.RemoveEmptyEntries)
|
||
If queryParts.Length = 0 Then
|
||
ResultList.AddRange(Entries)
|
||
Return ResultList
|
||
End If
|
||
|
||
' Precompute query parts in lowercase for case-insensitive comparison
|
||
Dim queryPartsLower As String() = queryParts.Select(Function(q) q.ToLower()).ToArray()
|
||
|
||
' Process each entry to compute similarity and absolute match status
|
||
For Each Entry In Entries
|
||
Entry.Similarity = SearchSimilarityWeighted(Entry.SearchSource, Query)
|
||
|
||
' Preprocess search source keys: remove spaces and convert to lowercase
|
||
Dim processedSources = Entry.SearchSource.Select(Function(s) s.Key.Replace(" ", "").ToLower()).ToList()
|
||
|
||
' Check if all query parts are matched exactly by at least one source
|
||
Dim isAbsoluteRight As Boolean = True
|
||
For Each qp In queryPartsLower
|
||
Dim found = False
|
||
For Each ps In processedSources
|
||
If ps.Contains(qp) Then
|
||
found = True
|
||
Exit For
|
||
End If
|
||
Next
|
||
If Not found Then
|
||
isAbsoluteRight = False
|
||
Exit For
|
||
End If
|
||
Next
|
||
Entry.AbsoluteRight = isAbsoluteRight
|
||
Next
|
||
|
||
' Sort by absolute match (descending), then by similarity (descending)
|
||
Dim sortedEntries = Entries.OrderByDescending(Function(e) e.AbsoluteRight).ThenByDescending(Function(e) e.Similarity).ToList()
|
||
|
||
' Build the final result list
|
||
Dim blurCount As Integer = 0
|
||
For Each Entry In sortedEntries
|
||
If Entry.AbsoluteRight Then
|
||
ResultList.Add(Entry)
|
||
Else
|
||
If Entry.Similarity < MinBlurSimilarity OrElse blurCount >= MaxBlurCount Then
|
||
Exit For
|
||
End If
|
||
ResultList.Add(Entry)
|
||
blurCount += 1
|
||
End If
|
||
Next
|
||
|
||
Return ResultList
|
||
End Function
|
||
|
||
#End Region
|
||
|
||
#Region "系统"
|
||
|
||
Public Function IsUtf8CodePage() As Boolean
|
||
Return Encoding.Default.CodePage = 65001
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 线程安全的 List。
|
||
''' 通过在 For Each 循环中使用一个浅表副本规避多线程操作或移除自身导致的异常。
|
||
''' </summary>
|
||
Public Class SafeList(Of T)
|
||
Inherits List(Of T)
|
||
Implements IEnumerable, IEnumerable(Of T)
|
||
|
||
Private ReadOnly SyncRoot As New Object
|
||
'构造函数
|
||
Public Sub New()
|
||
MyBase.New()
|
||
End Sub
|
||
Public Sub New(Data As IEnumerable(Of T))
|
||
MyBase.New(Data)
|
||
End Sub
|
||
Public Shared Function FromList(data As List(Of T)) As SafeList(Of T)
|
||
Return New SafeList(Of T)(data)
|
||
End Function
|
||
Public Function ToList() As List(Of T)
|
||
SyncLock SyncRoot
|
||
Return MyBase.ToList() ' 创建副本
|
||
End SyncLock
|
||
End Function
|
||
'基于 SyncLock 覆写
|
||
Public Overloads Function GetEnumerator() As IEnumerator(Of T) Implements IEnumerable(Of T).GetEnumerator
|
||
SyncLock SyncRoot
|
||
Return MyBase.ToList.GetEnumerator()
|
||
End SyncLock
|
||
End Function
|
||
Private Overloads Function GetEnumeratorGeneral() As IEnumerator Implements IEnumerable.GetEnumerator
|
||
SyncLock SyncRoot
|
||
Return MyBase.ToList.GetEnumerator()
|
||
End SyncLock
|
||
End Function
|
||
End Class
|
||
|
||
''' <summary>
|
||
''' 线程安全的字典。
|
||
''' 通过在 For Each 循环中使用一个浅表副本规避多线程操作或移除自身导致的异常。
|
||
''' </summary>
|
||
Public Class SafeDictionary(Of TKey, TValue)
|
||
Implements IDictionary(Of TKey, TValue)
|
||
Implements IEnumerable(Of KeyValuePair(Of TKey, TValue))
|
||
|
||
Private ReadOnly SyncRoot As New Object
|
||
Private ReadOnly _Dictionary As New Dictionary(Of TKey, TValue)
|
||
|
||
'构造函数
|
||
Public Sub New()
|
||
End Sub
|
||
Public Sub New(data As IEnumerable(Of KeyValuePair(Of TKey, TValue)))
|
||
For Each DataItem In data
|
||
_Dictionary.Add(DataItem.Key, DataItem.Value)
|
||
Next
|
||
End Sub
|
||
|
||
'线程安全的方法实现
|
||
Public Sub Add(key As TKey, value As TValue) Implements IDictionary(Of TKey, TValue).Add
|
||
SyncLock SyncRoot
|
||
_Dictionary.Add(key, value)
|
||
End SyncLock
|
||
End Sub
|
||
Public Function ContainsKey(key As TKey) As Boolean Implements IDictionary(Of TKey, TValue).ContainsKey
|
||
SyncLock SyncRoot
|
||
Return _Dictionary.ContainsKey(key)
|
||
End SyncLock
|
||
End Function
|
||
Public ReadOnly Property Keys As ICollection(Of TKey) Implements IDictionary(Of TKey, TValue).Keys
|
||
Get
|
||
SyncLock SyncRoot
|
||
Return New List(Of TKey)(_Dictionary.Keys)
|
||
End SyncLock
|
||
End Get
|
||
End Property
|
||
Public Function Remove(key As TKey) As Boolean Implements IDictionary(Of TKey, TValue).Remove
|
||
SyncLock SyncRoot
|
||
Return _Dictionary.Remove(key)
|
||
End SyncLock
|
||
End Function
|
||
Public Function TryGetValue(key As TKey, ByRef value As TValue) As Boolean Implements IDictionary(Of TKey, TValue).TryGetValue
|
||
SyncLock SyncRoot
|
||
Return _Dictionary.TryGetValue(key, value)
|
||
End SyncLock
|
||
End Function
|
||
Public ReadOnly Property Values As ICollection(Of TValue) Implements IDictionary(Of TKey, TValue).Values
|
||
Get
|
||
SyncLock SyncRoot
|
||
Return New List(Of TValue)(_Dictionary.Values)
|
||
End SyncLock
|
||
End Get
|
||
End Property
|
||
Default Public Property Item(key As TKey) As TValue Implements IDictionary(Of TKey, TValue).Item
|
||
Get
|
||
SyncLock SyncRoot
|
||
Return _Dictionary(key)
|
||
End SyncLock
|
||
End Get
|
||
Set(value As TValue)
|
||
SyncLock SyncRoot
|
||
_Dictionary(key) = value
|
||
End SyncLock
|
||
End Set
|
||
End Property
|
||
Public Sub Add(item As KeyValuePair(Of TKey, TValue)) Implements ICollection(Of KeyValuePair(Of TKey, TValue)).Add
|
||
SyncLock SyncRoot
|
||
_Dictionary.Add(item.Key, item.Value)
|
||
End SyncLock
|
||
End Sub
|
||
Public Sub Clear() Implements ICollection(Of KeyValuePair(Of TKey, TValue)).Clear
|
||
SyncLock SyncRoot
|
||
_Dictionary.Clear()
|
||
End SyncLock
|
||
End Sub
|
||
Public Function Contains(item As KeyValuePair(Of TKey, TValue)) As Boolean Implements ICollection(Of KeyValuePair(Of TKey, TValue)).Contains
|
||
SyncLock SyncRoot
|
||
Return DirectCast(_Dictionary, IDictionary(Of TKey, TValue)).Contains(item)
|
||
End SyncLock
|
||
End Function
|
||
Public Sub CopyTo(array() As KeyValuePair(Of TKey, TValue), arrayIndex As Integer) Implements ICollection(Of KeyValuePair(Of TKey, TValue)).CopyTo
|
||
SyncLock SyncRoot
|
||
DirectCast(_Dictionary, IDictionary(Of TKey, TValue)).CopyTo(array, arrayIndex)
|
||
End SyncLock
|
||
End Sub
|
||
Public ReadOnly Property Count As Integer Implements ICollection(Of KeyValuePair(Of TKey, TValue)).Count
|
||
Get
|
||
SyncLock SyncRoot
|
||
Return _Dictionary.Count
|
||
End SyncLock
|
||
End Get
|
||
End Property
|
||
Public ReadOnly Property IsReadOnly As Boolean Implements ICollection(Of KeyValuePair(Of TKey, TValue)).IsReadOnly
|
||
Get
|
||
Return False
|
||
End Get
|
||
End Property
|
||
Public Function Remove(item As KeyValuePair(Of TKey, TValue)) As Boolean Implements ICollection(Of KeyValuePair(Of TKey, TValue)).Remove
|
||
SyncLock SyncRoot
|
||
Return DirectCast(_Dictionary, IDictionary(Of TKey, TValue)).Remove(item)
|
||
End SyncLock
|
||
End Function
|
||
|
||
'枚举器
|
||
Public Function GetEnumerator() As IEnumerator(Of KeyValuePair(Of TKey, TValue)) Implements IEnumerable(Of KeyValuePair(Of TKey, TValue)).GetEnumerator
|
||
SyncLock SyncRoot
|
||
Return New List(Of KeyValuePair(Of TKey, TValue))(_Dictionary).GetEnumerator()
|
||
End SyncLock
|
||
End Function
|
||
Private Function GetEnumeratorGeneral() As IEnumerator Implements IEnumerable.GetEnumerator
|
||
Return GetEnumerator()
|
||
End Function
|
||
End Class
|
||
|
||
''' <summary>
|
||
''' 可用于临时存放文件的,不含任何特殊字符的文件夹路径,以“\”结尾。
|
||
''' </summary>
|
||
Public PathPure As String = GetPureASCIIDir()
|
||
Private Function GetPureASCIIDir() As String
|
||
If ExePath.IsASCII() Then
|
||
Return ExePath & "PCL\"
|
||
ElseIf PathAppdata.IsASCII() Then
|
||
Return PathAppdata
|
||
ElseIf PathTemp.IsASCII() Then
|
||
Return PathTemp
|
||
Else
|
||
Return OsDrive & "ProgramData\PCL\"
|
||
End If
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 指示接取到这个异常的函数进行重试。
|
||
''' </summary>
|
||
Public Class RestartException
|
||
Inherits Exception
|
||
End Class
|
||
''' <summary>
|
||
''' 指示用户手动取消了操作,或用户已知晓操作被取消的原因。
|
||
''' </summary>
|
||
Public Class CancelledException
|
||
Inherits Exception
|
||
End Class
|
||
|
||
Private Uuid As Integer = 1
|
||
Private UuidLock As Object
|
||
''' <summary>
|
||
''' 获取一个全程序内不会重复的数字(伪 Uuid)。
|
||
''' </summary>
|
||
Public Function GetUuid() As Integer
|
||
If UuidLock Is Nothing Then UuidLock = New Object
|
||
SyncLock UuidLock
|
||
Uuid += 1
|
||
Return Uuid
|
||
End SyncLock
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 将元素与 List 的混合体拆分为元素组。
|
||
''' </summary>
|
||
Public Function GetFullList(Of T)(data As IList) As List(Of T)
|
||
GetFullList = New List(Of T)
|
||
For i = 0 To data.Count - 1
|
||
If TypeOf data(i) Is ICollection Then
|
||
GetFullList.AddRange(data(i))
|
||
Else
|
||
GetFullList.Add(data(i))
|
||
End If
|
||
Next i
|
||
End Function
|
||
''' <summary>
|
||
''' 数组去重。
|
||
''' </summary>
|
||
<Extension> Public Function Distinct(Of T)(Arr As ICollection(Of T), IsEqual As ComparisonBoolean(Of T)) As List(Of T)
|
||
Dim ResultArray As New List(Of T)
|
||
For i = 0 To Arr.Count - 1
|
||
For ii = i + 1 To Arr.Count - 1
|
||
If IsEqual(Arr(i), Arr(ii)) Then GoTo NextElement
|
||
Next
|
||
ResultArray.Add(Arr(i))
|
||
NextElement:
|
||
Next i
|
||
Return ResultArray
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 用于储存 RaiseByMouse 的 EventArgs。
|
||
''' </summary>
|
||
Public NotInheritable Class RouteEventArgs
|
||
Inherits EventArgs
|
||
Public RaiseByMouse As Boolean
|
||
Public Handled As Boolean = False
|
||
Public Sub New(Optional RaiseByMouse As Boolean = False)
|
||
Me.RaiseByMouse = RaiseByMouse
|
||
End Sub
|
||
End Class
|
||
|
||
''' <summary>
|
||
''' 前台运行文件。
|
||
''' </summary>
|
||
''' <param name="FileName">文件名。可以为“notepad”等缩写。</param>
|
||
''' <param name="Arguments">运行参数。</param>
|
||
Public Sub ShellOnly(FileName As String, Optional Arguments As String = "")
|
||
Try
|
||
FileName = ShortenPath(FileName)
|
||
Using Program As New Process
|
||
Program.StartInfo.Arguments = Arguments
|
||
Program.StartInfo.FileName = FileName
|
||
Program.StartInfo.UseShellExecute = True
|
||
Log("[System] 执行外部命令:" & FileName & " " & Arguments)
|
||
Program.Start()
|
||
End Using
|
||
Catch ex As Exception
|
||
Log(ex, "打开文件或程序失败:" & FileName, LogLevel.Msgbox)
|
||
End Try
|
||
End Sub
|
||
''' <summary>
|
||
''' 前台运行文件并返回返回值。
|
||
''' </summary>
|
||
''' <param name="FileName">文件名。可以为“notepad”等缩写。</param>
|
||
''' <param name="Arguments">运行参数。</param>
|
||
''' <param name="Timeout">等待该程序结束的最长时间(毫秒)。超时会返回 Result.Timeout。</param>
|
||
Public Function ShellAndGetExitCode(FileName As String, Optional Arguments As String = "", Optional Timeout As Integer = 1000000) As ProcessReturnValues
|
||
Try
|
||
Using Program As New Process
|
||
Program.StartInfo.Arguments = Arguments
|
||
Program.StartInfo.FileName = FileName
|
||
Log("[System] 执行外部命令并等待返回码:" & FileName & " " & Arguments)
|
||
Program.Start()
|
||
If Program.WaitForExit(Timeout) Then
|
||
Return Program.ExitCode
|
||
Else
|
||
Return ProcessReturnValues.Timeout
|
||
End If
|
||
End Using
|
||
Catch ex As Exception
|
||
Log(ex, "执行命令失败:" & FileName, LogLevel.Msgbox)
|
||
Return ProcessReturnValues.Fail
|
||
End Try
|
||
End Function
|
||
''' <summary>
|
||
''' 静默运行文件并返回输出流字符串。执行失败会抛出异常。
|
||
''' </summary>
|
||
''' <param name="FileName">文件名。可以为“notepad”等缩写。</param>
|
||
''' <param name="Arguments">运行参数。</param>
|
||
''' <param name="Timeout">等待该程序结束的最长时间(毫秒)。超时会抛出错误。</param>
|
||
Public Function ShellAndGetOutput(FileName As String, Optional Arguments As String = "", Optional Timeout As Integer = 1000000, Optional WorkingDirectory As String = Nothing) As String
|
||
Dim Info = New ProcessStartInfo With {
|
||
.FileName = FileName,
|
||
.Arguments = Arguments,
|
||
.UseShellExecute = False,
|
||
.CreateNoWindow = True,
|
||
.RedirectStandardOutput = True,
|
||
.RedirectStandardError = True
|
||
}
|
||
|
||
' 设置工作目录(如果提供)
|
||
If Not String.IsNullOrEmpty(WorkingDirectory) Then
|
||
Info.WorkingDirectory = WorkingDirectory.TrimEnd("\")
|
||
End If
|
||
|
||
Log("[System] 执行外部命令并等待返回结果:" & FileName & " " & Arguments)
|
||
|
||
Using Program As New Process() With {.StartInfo = Info}
|
||
Program.Start()
|
||
|
||
' 异步读取输出和错误流
|
||
Dim outputTask = Program.StandardOutput.ReadToEndAsync()
|
||
Dim errorTask = Program.StandardError.ReadToEndAsync()
|
||
|
||
' 等待进程退出或超时
|
||
If Program.WaitForExit(Timeout) Then
|
||
' 确保异步读取完成
|
||
Task.WaitAll(outputTask, errorTask)
|
||
Else
|
||
' 超时后终止进程
|
||
Program.Kill()
|
||
' 仍然尝试获取已输出的内容
|
||
Task.WaitAll(outputTask, errorTask)
|
||
End If
|
||
|
||
' 合并结果并返回
|
||
Return outputTask.Result & errorTask.Result
|
||
End Using
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 在新的工作线程中执行代码。
|
||
''' </summary>
|
||
Public Function RunInNewThread(Action As Action, Optional Name As String = Nothing, Optional Priority As ThreadPriority = ThreadPriority.Normal) As Thread
|
||
Dim th As New Thread(
|
||
Sub()
|
||
Try
|
||
Action()
|
||
Catch ex As ThreadInterruptedException
|
||
Log(Name & ":线程已中止")
|
||
Catch ex As Exception
|
||
Log(ex, Name & ":线程执行失败", LogLevel.Feedback)
|
||
End Try
|
||
End Sub) With {.Name = If(Name, "Runtime New Invoke " & GetUuid() & "#"), .Priority = Priority}
|
||
th.Start()
|
||
Return th
|
||
End Function
|
||
''' <summary>
|
||
''' 确保在 UI 线程中执行代码。
|
||
''' 如果当前并非 UI 线程,则会阻断当前线程,直至 UI 线程执行完毕。
|
||
''' 为防止线程互锁,请仅在开始加载动画、从 UI 获取输入时使用!
|
||
''' </summary>
|
||
Public Function RunInUiWait(Of Output)(Action As Func(Of Output)) As Output
|
||
If RunInUi() Then
|
||
Return Action()
|
||
Else
|
||
Return Application.Current.Dispatcher.Invoke(Action)
|
||
End If
|
||
End Function
|
||
''' <summary>
|
||
''' 确保在 UI 线程中执行代码。
|
||
''' 如果当前并非 UI 线程,则会阻断当前线程,直至 UI 线程执行完毕。
|
||
''' 为防止线程互锁,请仅在开始加载动画、从 UI 获取输入时使用!
|
||
''' </summary>
|
||
Public Sub RunInUiWait(Action As Action)
|
||
If Application.Current Is Nothing Then Exit Sub
|
||
If RunInUi() Then
|
||
Action()
|
||
Else
|
||
Application.Current.Dispatcher.Invoke(Action)
|
||
End If
|
||
End Sub
|
||
''' <summary>
|
||
''' 确保在 UI 线程中执行代码,代码按触发顺序执行。
|
||
''' 如果当前并非 UI 线程,也不阻断当前线程的执行。
|
||
''' </summary>
|
||
Public Sub RunInUi(Action As Action, Optional ForceWaitUntilLoaded As Boolean = False)
|
||
If Application.Current Is Nothing Then Exit Sub
|
||
If RunInUi() Then
|
||
Action()
|
||
Else
|
||
Application.Current.Dispatcher.InvokeAsync(Action, If(ForceWaitUntilLoaded, Threading.DispatcherPriority.Loaded, Threading.DispatcherPriority.Normal))
|
||
End If
|
||
End Sub
|
||
''' <summary>
|
||
''' 确保在工作线程中执行代码。
|
||
''' </summary>
|
||
Public Sub RunInThread(Action As Action)
|
||
If RunInUi() Then
|
||
RunInNewThread(Action, "Runtime Invoke " & GetUuid() & "#")
|
||
Else
|
||
Action()
|
||
End If
|
||
End Sub
|
||
|
||
''' <summary>
|
||
''' 使用优化的归并排序算法进行稳定排序。
|
||
''' </summary>
|
||
''' <param name="SortRule">传入两个对象,若第一个对象应该排在前面,则返回 True。</param>
|
||
<Extension>
|
||
Public Function Sort(Of T)(List As IList(Of T), SortRule As ComparisonBoolean(Of T)) As List(Of T)
|
||
' 创建原列表的副本以避免修改原始列表
|
||
Dim tempList As New List(Of T)(List)
|
||
If tempList.Count <= 1 Then Return tempList
|
||
|
||
' 使用归并排序核心算法
|
||
MergeSort_Sort(tempList, 0, tempList.Count - 1, SortRule)
|
||
Return tempList
|
||
End Function
|
||
|
||
Private Sub MergeSort_Sort(Of T)(ByRef array As List(Of T), left As Integer, right As Integer, comparator As ComparisonBoolean(Of T))
|
||
If left >= right Then Return
|
||
|
||
Dim mid As Integer = (left + right) \ 2
|
||
MergeSort_Sort(array, left, mid, comparator)
|
||
MergeSort_Sort(array, mid + 1, right, comparator)
|
||
MergeSort_Merge(array, left, mid, right, comparator)
|
||
End Sub
|
||
|
||
Private Sub MergeSort_Merge(Of T)(ByRef array As List(Of T), left As Integer, mid As Integer, right As Integer, comparator As ComparisonBoolean(Of T))
|
||
Dim leftArray As New List(Of T)
|
||
Dim rightArray As New List(Of T)
|
||
|
||
For i As Integer = left To mid
|
||
leftArray.Add(array(i))
|
||
Next
|
||
|
||
For j As Integer = mid + 1 To right
|
||
rightArray.Add(array(j))
|
||
Next
|
||
|
||
Dim leftPtr = 0, rightPtr = 0, current = left
|
||
|
||
While leftPtr < leftArray.Count AndAlso rightPtr < rightArray.Count
|
||
' 保持稳定性的关键比较逻辑:当相等时优先取左数组元素
|
||
If comparator(leftArray(leftPtr), rightArray(rightPtr)) Then
|
||
array(current) = leftArray(leftPtr)
|
||
leftPtr += 1
|
||
Else
|
||
array(current) = rightArray(rightPtr)
|
||
rightPtr += 1
|
||
End If
|
||
current += 1
|
||
End While
|
||
|
||
While leftPtr < leftArray.Count
|
||
array(current) = leftArray(leftPtr)
|
||
leftPtr += 1
|
||
current += 1
|
||
End While
|
||
|
||
While rightPtr < rightArray.Count
|
||
array(current) = rightArray(rightPtr)
|
||
rightPtr += 1
|
||
current += 1
|
||
End While
|
||
End Sub
|
||
|
||
Public Delegate Function ComparisonBoolean(Of T)(Left As T, Right As T) As Boolean
|
||
|
||
''' <summary>
|
||
''' 返回列表的浅表副本。
|
||
''' </summary>
|
||
<Extension> Public Function Clone(Of T)(list As IList(Of T)) As IList(Of T)
|
||
Return New List(Of T)(list)
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 尝试从字典中获取某项,如果该项不存在,则返回默认值。
|
||
''' </summary>
|
||
<Extension> Public Function GetOrDefault(Of TKey, TValue)(Dict As Dictionary(Of TKey, TValue), Key As TKey, Optional DefaultValue As TValue = Nothing) As TValue
|
||
If Dict.ContainsKey(Key) Then
|
||
Return Dict(Key)
|
||
Else
|
||
Return DefaultValue
|
||
End If
|
||
End Function
|
||
''' <summary>
|
||
''' 将某项添加到以列表作为值的字典中。
|
||
''' </summary>
|
||
<Extension> Public Sub AddToList(Of TKey, TValue)(Dict As Dictionary(Of TKey, List(Of TValue)), Key As TKey, Value As TValue)
|
||
If Dict.ContainsKey(Key) Then
|
||
Dict(Key).Add(Value)
|
||
Else
|
||
Dict.Add(Key, New List(Of TValue) From {Value})
|
||
End If
|
||
End Sub
|
||
|
||
''' <summary>
|
||
''' 获取程序启动参数。
|
||
''' </summary>
|
||
''' <param name="Name">参数名。</param>
|
||
''' <param name="DefaultValue">默认值。</param>
|
||
Public Function GetProgramArgument(Name As String, Optional DefaultValue As Object = "")
|
||
Dim AllArguments() As String = Command.Split(" ")
|
||
For i = 0 To AllArguments.Length - 1
|
||
If AllArguments(i) = "-" & Name Then
|
||
If AllArguments.Length = i + 1 OrElse AllArguments(i + 1).StartsWithF("-") Then Return True
|
||
Return AllArguments(i + 1)
|
||
End If
|
||
Next
|
||
Return DefaultValue
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 打开网页。
|
||
''' </summary>
|
||
Public Sub OpenWebsite(Url As String)
|
||
Try
|
||
If Not Url.StartsWithF("http", True) AndAlso Not Url.StartsWithF("minecraft://", True) Then
|
||
Throw New Exception(Url & " 不是一个有效的网址,它必须以 http 开头!")
|
||
End If
|
||
Log("[System] 正在打开网页:" & Url)
|
||
Basics.OpenPath(Url)
|
||
Catch ex As Exception
|
||
Log(ex, "无法打开网页(" & Url & ")")
|
||
ClipboardSet(Url, False)
|
||
MyMsgBox("可能由于浏览器未正确配置,PCL 无法为你打开网页。" & vbCrLf & "网址已经复制到剪贴板,若有需要可以手动粘贴访问。" & vbCrLf &
|
||
$"网址:{Url}", "无法打开网页")
|
||
End Try
|
||
End Sub
|
||
''' <summary>
|
||
''' 打开 explorer。
|
||
''' 若不以 \ 结尾,则将视作文件路径,打开并选中此文件。
|
||
''' </summary>
|
||
Public Sub OpenExplorer(Location As String)
|
||
Try
|
||
Location = ShortenPath(Location.Replace("/", "\").Trim(" "c, """"c))
|
||
Log("[System] 正在打开资源管理器:" & Location)
|
||
If Location.EndsWithF("\") Then
|
||
ShellOnly(Location)
|
||
Else
|
||
ShellOnly("explorer", $"/select,""{Location}""")
|
||
End If
|
||
Catch ex As Exception
|
||
Log(ex, "打开资源管理器失败,请尝试关闭安全软件(如 360 安全卫士)", LogLevel.Msgbox)
|
||
End Try
|
||
End Sub
|
||
|
||
''' <summary>
|
||
''' 设置剪贴板。将在另一线程运行,且不会抛出异常。
|
||
''' </summary>
|
||
Public Sub ClipboardSet(Text As String, Optional ShowSuccessHint As Boolean = True)
|
||
RunInThread(Sub()
|
||
Dim success As Boolean = False
|
||
|
||
For attempt As Integer = 0 To 5
|
||
Try
|
||
RunInUi(Sub()
|
||
Clipboard.SetText(Text)
|
||
End Sub)
|
||
success = True
|
||
Exit For
|
||
Catch ex As Exception When attempt < 5
|
||
Thread.Sleep(20)
|
||
Catch finalEx As Exception
|
||
Log(finalEx, "剪贴板被占用,文本复制失败", LogLevel.Hint)
|
||
End Try
|
||
Next
|
||
|
||
If success AndAlso ShowSuccessHint Then
|
||
RunInUi(Sub() Hint("已成功复制!", HintType.Finish))
|
||
End If
|
||
End Sub)
|
||
End Sub
|
||
|
||
''' <summary>
|
||
''' 从剪切板粘贴文件或文件夹
|
||
''' </summary>
|
||
''' <param name="dest">目标文件夹</param>
|
||
''' <param name="copyFile">是否粘贴文件</param>
|
||
''' <param name="copyDir">是否粘贴文件夹</param>
|
||
''' <returns>总共粘贴的数量</returns>
|
||
Public Function PasteFileFromClipboard(dest As String, Optional copyFile As Boolean = True, Optional copyDir As Boolean = True) As Integer
|
||
Log("[System] 从剪贴板粘贴文件到:" & dest)
|
||
Try
|
||
Dim files As Specialized.StringCollection = Clipboard.GetFileDropList()
|
||
If files.Count.Equals(0) Then
|
||
Log("[System] 剪贴板内无文件可粘贴")
|
||
Return 0
|
||
End If
|
||
Dim CopiedFiles = 0
|
||
Dim CopiedFolders = 0
|
||
For Each i In files
|
||
If copyFile AndAlso File.Exists(i) Then '文件
|
||
Try
|
||
Dim thisDest = dest & GetFileNameFromPath(i)
|
||
If File.Exists(thisDest) Then
|
||
Log("[System] 已存在同名文件:" & thisDest)
|
||
Else
|
||
File.Copy(i, thisDest)
|
||
CopiedFiles += 1
|
||
End If
|
||
Catch ex As Exception
|
||
Log(ex, "[System] 复制文件时出错")
|
||
Continue For
|
||
End Try
|
||
End If
|
||
If copyDir AndAlso Directory.Exists(i) Then '文件夹
|
||
Try
|
||
Dim thisDest = dest & GetFolderNameFromPath(i)
|
||
If Directory.Exists(thisDest) Then
|
||
Log("[System] 已存在同名文件夹:" & thisDest)
|
||
Else
|
||
CopyDirectory(i, thisDest)
|
||
CopiedFolders += 1
|
||
End If
|
||
Catch ex As Exception
|
||
Log(ex, "[System] 复制文件时出错")
|
||
Continue For
|
||
End Try
|
||
End If
|
||
Next
|
||
Hint("[System] 已粘贴 " & CopiedFiles & " 个文件和 " & CopiedFolders & " 个文件夹")
|
||
Catch ex As Exception
|
||
Log(ex, "[System] 从剪切板粘贴文件失败", LogLevel.Hint)
|
||
End Try
|
||
Return 0
|
||
End Function
|
||
|
||
''' <summary>
|
||
''' 获取程序打包资源的输入流。该资源必须声明为 <c>Resource</c> 类型,否则将会报错,<c>Images</c>
|
||
''' 和 <c>Resources</c> 目录已默认声明该类型。
|
||
''' </summary>
|
||
Public Function GetResourceStream(path As String) As Stream
|
||
Dim resourceInfo = Application.GetResourceStream(New Uri($"pack://application:,,,/{path}", UriKind.Absolute))
|
||
Return resourceInfo?.Stream
|
||
End Function
|
||
|
||
#End Region
|
||
|
||
''' 检查是否拥有某一文件夹的 I/O 权限。如果文件夹不存在,会返回 False。
|
||
''' </summary>
|
||
Public Function CheckPermission(Path As String) As Boolean
|
||
Try
|
||
If String.IsNullOrEmpty(Path) Then Return False
|
||
If Not Path.EndsWithF("\") Then Path += "\"
|
||
If Path.EndsWithF(":\System Volume Information\") OrElse Path.EndsWithF(":\$RECYCLE.BIN\") Then Return False
|
||
If Not Directory.Exists(Path) Then Return False
|
||
Dim FileName As String = "CheckPermission" & GetUuid()
|
||
If File.Exists(Path & FileName) Then File.Delete(Path & FileName)
|
||
File.Create(Path & FileName).Dispose()
|
||
File.Delete(Path & FileName)
|
||
Return True
|
||
Catch ex As Exception
|
||
Log(ex, "没有对文件夹 " & Path & " 的权限,请尝试以管理员权限运行 PCL")
|
||
Return False
|
||
End Try
|
||
End Function
|
||
''' <summary>
|
||
''' 检查是否拥有某一文件夹的 I/O 权限。如果出错,则抛出异常。
|
||
''' </summary>
|
||
Public Sub CheckPermissionWithException(Path As String)
|
||
If String.IsNullOrWhiteSpace(Path) Then Throw New ArgumentNullException("文件夹名不能为空!")
|
||
If Not Path.EndsWithF("\") Then Path += "\"
|
||
If Not Directory.Exists(Path) Then Throw New DirectoryNotFoundException("文件夹不存在!")
|
||
If File.Exists(Path & "CheckPermission") Then File.Delete(Path & "CheckPermission")
|
||
File.Create(Path & "CheckPermission").Dispose()
|
||
File.Delete(Path & "CheckPermission")
|
||
End Sub
|
||
''' <summary>
|
||
|
||
#Region "UI"
|
||
|
||
Public Sub SetLaunchFont(Optional FontName As String = Nothing)
|
||
Try
|
||
Dim TargetFont As FontFamily
|
||
If String.IsNullOrEmpty(FontName) Then
|
||
TargetFont = New FontFamily(New Uri("pack://application:,,,/"), "./Resources/#PCL English, Segoe UI, Microsoft YaHei UI")
|
||
Else
|
||
TargetFont = New FontFamily($"{FontName}, Segoe UI, Microsoft YaHei UI")
|
||
End If
|
||
Application.Current.Resources("LaunchFontFamily") = TargetFont
|
||
Catch ex As Exception
|
||
Log(ex, "设置字体失败", LogLevel.Hint)
|
||
End Try
|
||
End Sub
|
||
|
||
'边距改变
|
||
''' <summary>
|
||
''' 相对增减控件的左边距。
|
||
''' </summary>
|
||
Public Sub DeltaLeft(control As FrameworkElement, newValue As Double)
|
||
'安全性检查
|
||
DebugAssert(Not Double.IsNaN(newValue))
|
||
DebugAssert(Not Double.IsInfinity(newValue))
|
||
|
||
If TypeOf control Is Window Then
|
||
'窗口改变
|
||
CType(control, Window).Left += newValue
|
||
Else
|
||
'根据 HorizontalAlignment 改变数值
|
||
Select Case control.HorizontalAlignment
|
||
Case HorizontalAlignment.Left, HorizontalAlignment.Stretch
|
||
control.Margin = New Thickness(control.Margin.Left + newValue, control.Margin.Top, control.Margin.Right, control.Margin.Bottom)
|
||
Case HorizontalAlignment.Right
|
||
control.Margin = New Thickness(control.Margin.Left, control.Margin.Top, control.Margin.Right - newValue, control.Margin.Bottom)
|
||
'control.Margin = New Thickness(control.Margin.Left, control.Margin.Top, CType(control.Parent, Object).ActualWidth - control.ActualWidth - newValue, control.Margin.Bottom)
|
||
Case Else
|
||
DebugAssert(False)
|
||
End Select
|
||
End If
|
||
End Sub
|
||
''' <summary>
|
||
''' 设置控件的左边距。(仅针对置左控件)
|
||
''' </summary>
|
||
Public Sub SetLeft(control As FrameworkElement, newValue As Double)
|
||
DebugAssert(control.HorizontalAlignment = HorizontalAlignment.Left)
|
||
control.Margin = New Thickness(newValue, control.Margin.Top, control.Margin.Right, control.Margin.Bottom)
|
||
End Sub
|
||
''' <summary>
|
||
''' 相对增减控件的上边距。
|
||
''' </summary>
|
||
Public Sub DeltaTop(control As FrameworkElement, newValue As Double)
|
||
'安全性检查
|
||
DebugAssert(Not Double.IsNaN(newValue))
|
||
DebugAssert(Not Double.IsInfinity(newValue))
|
||
|
||
If TypeOf control Is Window Then
|
||
'窗口改变
|
||
CType(control, Window).Top += newValue
|
||
Else
|
||
'根据 VerticalAlignment 改变数值
|
||
Select Case control.VerticalAlignment
|
||
Case VerticalAlignment.Top
|
||
control.Margin = New Thickness(control.Margin.Left, control.Margin.Top + newValue, control.Margin.Right, control.Margin.Bottom)
|
||
Case VerticalAlignment.Bottom
|
||
control.Margin = New Thickness(control.Margin.Left, control.Margin.Top, control.Margin.Right, control.Margin.Bottom - newValue)
|
||
'control.Margin = New Thickness(control.Margin.Left, control.Margin.Top, CType(control.Parent, Object).ActualWidth - control.ActualWidth - newValue, control.Margin.Bottom)
|
||
Case Else
|
||
DebugAssert(False)
|
||
End Select
|
||
End If
|
||
|
||
'If Double.IsNaN(newValue) OrElse Double.IsInfinity(newValue) Then Return '安全性检查
|
||
'Select Case control.VerticalAlignment
|
||
' Case VerticalAlignment.Top, VerticalAlignment.Stretch, VerticalAlignment.Center
|
||
' control.Margin = New Thickness(control.Margin.Left, newValue, control.Margin.Right, control.Margin.Bottom)
|
||
' Case VerticalAlignment.Bottom
|
||
' control.Margin = New Thickness(control.Margin.Left, control.Margin.Top, control.Margin.Right, -newValue)
|
||
' 'control.Margin = New Thickness(control.Margin.Left, control.Margin.Top, control.Margin.Right, CType(control.Parent, Object).ActualHeight - control.ActualHeight - newValue)
|
||
'End Select
|
||
End Sub
|
||
''' <summary>
|
||
''' 设置控件的顶边距。(仅针对置上控件)
|
||
''' </summary>
|
||
Public Sub SetTop(control As FrameworkElement, newValue As Double)
|
||
DebugAssert(control.VerticalAlignment = VerticalAlignment.Top)
|
||
control.Margin = New Thickness(control.Margin.Left, newValue, control.Margin.Right, control.Margin.Bottom)
|
||
End Sub
|
||
|
||
'DPI 转换
|
||
Public ReadOnly DPI As Integer = System.Drawing.Graphics.FromHwnd(IntPtr.Zero).DpiX
|
||
''' <summary>
|
||
''' 将经过 DPI 缩放的 WPF 尺寸转化为实际的像素尺寸。
|
||
''' </summary>
|
||
Public Function GetPixelSize(WPFSize As Double) As Double
|
||
Return WPFSize / 96 * DPI
|
||
End Function
|
||
''' <summary>
|
||
''' 将实际的像素尺寸转化为经过 DPI 缩放的 WPF 尺寸。
|
||
''' </summary>
|
||
Public Function GetWPFSize(PixelSize As Double) As Double
|
||
Return PixelSize * 96 / DPI
|
||
End Function
|
||
|
||
'UI 截图
|
||
''' <summary>
|
||
''' 将某个控件的呈现转换为图片。
|
||
''' </summary>
|
||
Public Function ControlBrush(UI As FrameworkElement) As ImageBrush
|
||
Dim Width = UI.ActualWidth, Height = UI.ActualHeight
|
||
If Width < 1 OrElse Height < 1 Then Return New ImageBrush
|
||
Dim bmp As New RenderTargetBitmap(GetPixelSize(Width), GetPixelSize(Height), DPI, DPI, PixelFormats.Pbgra32)
|
||
bmp.Render(UI)
|
||
Return New ImageBrush(bmp)
|
||
End Function
|
||
''' <summary>
|
||
''' 将某个控件的模拟呈现转换为图片。
|
||
''' </summary>
|
||
Public Function ControlBrush(UI As FrameworkElement, Width As Double, Height As Double, Optional Left As Double = 0, Optional Top As Double = 0) As ImageBrush
|
||
UI.Measure(New Size(Width, Height))
|
||
UI.Arrange(New Rect(0, 0, Width, Height))
|
||
Dim bmp As New RenderTargetBitmap(GetPixelSize(Width), GetPixelSize(Height), DPI, DPI, PixelFormats.Default)
|
||
bmp.Render(UI)
|
||
If Not (Left = 0 AndAlso Top = 0) Then UI.Arrange(New Rect(Left, Top, Width, Height))
|
||
Return New ImageBrush(bmp)
|
||
End Function
|
||
''' <summary>
|
||
''' 将 UI 内容固定为图片并进行 Clear。
|
||
''' </summary>
|
||
Public Sub ControlFreeze(UI As Panel)
|
||
UI.Background = ControlBrush(UI)
|
||
UI.Children.Clear()
|
||
End Sub
|
||
''' <summary>
|
||
''' 将 UI 内容固定为图片并进行 Clear。
|
||
''' </summary>
|
||
Public Sub ControlFreeze(UI As Border)
|
||
UI.Background = ControlBrush(UI)
|
||
UI.Child = Nothing
|
||
End Sub
|
||
|
||
''' <summary>
|
||
''' 将 XML 转换为对应 UI 对象。
|
||
''' </summary>
|
||
Public Function GetObjectFromXML(Str As XElement)
|
||
Return GetObjectFromXML(Str.ToString)
|
||
End Function
|
||
''' <summary>
|
||
''' 将 XML 转换为对应 UI 对象。
|
||
''' </summary>
|
||
Public Function GetObjectFromXML(Str As String) As Object
|
||
Using Stream As New MemoryStream(Encoding.UTF8.GetBytes(Str))
|
||
'类型检查
|
||
Using Reader As New XamlXmlReader(Stream)
|
||
While Reader.Read()
|
||
For Each BlackListType In {GetType(WebBrowser), GetType(Frame), GetType(MediaElement), GetType(ObjectDataProvider), GetType(XamlReader), GetType(Window), GetType(XmlDataProvider)}
|
||
If Reader.Type IsNot Nothing AndAlso BlackListType.IsAssignableFrom(Reader.Type.UnderlyingType) Then Throw New UnauthorizedAccessException($"不允许使用 {BlackListType.Name} 类型。")
|
||
If Reader.Value IsNot Nothing AndAlso Reader.Value = BlackListType.Name Then Throw New UnauthorizedAccessException($"不允许使用 {BlackListType.Name} 值。")
|
||
Next
|
||
For Each BlackListMember In {"Code", "FactoryMethod", "Static"}
|
||
If Reader.Member IsNot Nothing AndAlso Reader.Member.Name = BlackListMember Then Throw New UnauthorizedAccessException($"不允许使用 {BlackListMember} 成员。")
|
||
Next
|
||
End While
|
||
End Using
|
||
'实际的加载
|
||
Stream.Position = 0
|
||
Using Writer As New StreamWriter(Stream)
|
||
Writer.Write(Str)
|
||
Writer.Flush()
|
||
Stream.Position = 0
|
||
Return Markup.XamlReader.Load(Stream)
|
||
End Using
|
||
End Using
|
||
End Function
|
||
|
||
Private ReadOnly UiThreadId As Integer = Thread.CurrentThread.ManagedThreadId
|
||
''' <summary>
|
||
''' 当前线程是否为主线程。
|
||
''' </summary>
|
||
Public Function RunInUi() As Boolean
|
||
Return Thread.CurrentThread.ManagedThreadId = UiThreadId
|
||
End Function
|
||
|
||
#End Region
|
||
|
||
#Region "Debug"
|
||
|
||
Public ModeDebug As Boolean = False
|
||
|
||
'Log
|
||
Public Enum LogLevel
|
||
''' <summary>
|
||
''' 不提示,只记录日志。
|
||
''' </summary>
|
||
Normal = 0
|
||
''' <summary>
|
||
''' 只提示开发者。
|
||
''' </summary>
|
||
Developer = 1
|
||
''' <summary>
|
||
''' 只提示开发者与调试模式用户。
|
||
''' </summary>
|
||
Debug = 2
|
||
''' <summary>
|
||
''' 弹出提示所有用户。
|
||
''' </summary>
|
||
Hint = 3
|
||
''' <summary>
|
||
''' 弹窗,不要求反馈。
|
||
''' </summary>
|
||
Msgbox = 4
|
||
''' <summary>
|
||
''' 弹窗,要求反馈。
|
||
''' </summary>
|
||
Feedback = 5
|
||
''' <summary>
|
||
''' 弹出 Windows 原生弹窗,要求反馈。在无法保证 WPF 窗口能正常运行时使用此级别。
|
||
''' 在第二次触发后会直接结束程序。
|
||
''' </summary>
|
||
Critical = 6
|
||
End Enum
|
||
Private IsCriticalErrorTriggered As Boolean = False
|
||
''' <summary>
|
||
''' 输出 Log。
|
||
''' </summary>
|
||
''' <param name="Title">如果要求弹窗,指定弹窗的标题。</param>
|
||
Public Sub Log(Text As String, Optional Level As LogLevel = LogLevel.Normal, Optional Title As String = "出现错误")
|
||
'On Error Resume Next
|
||
'放在最后会导致无法显示极端错误下的弹窗(如无法写入日志文件)
|
||
'处理错误会导致再次调用 Log() 导致无限循环
|
||
|
||
'输出日志
|
||
If {LogLevel.Msgbox, LogLevel.Hint}.Contains(Level) Then
|
||
LogWrapper.Warn(Text)
|
||
ElseIf LogLevel.Feedback = Level Then
|
||
LogWrapper.Error(Text)
|
||
ElseIf LogLevel.Critical = Level Then
|
||
LogWrapper.Fatal(Text)
|
||
ElseIf LogLevel.Debug = Level Then
|
||
LogWrapper.Debug(Text)
|
||
ElseIf LogLevel.Developer = Level Then
|
||
LogWrapper.Trace(Text)
|
||
Else
|
||
LogWrapper.Info(Text)
|
||
End If
|
||
|
||
If IsProgramEnded OrElse Level = LogLevel.Normal Then Return
|
||
|
||
'去除前缀
|
||
Text = Text.RegexReplace("\[[^\]]+?\] ", "")
|
||
|
||
'输出提示
|
||
Select Case Level
|
||
#If DEBUGRESERVED Then
|
||
Case LogLevel.Developer
|
||
Hint("[开发者模式] " & Text, HintType.Info, False)
|
||
Case LogLevel.Debug
|
||
Hint("[调试模式] " & Text, HintType.Info, False)
|
||
#Else
|
||
Case LogLevel.Developer
|
||
Case LogLevel.Debug
|
||
If ModeDebug Then Hint("[调试模式] " & Text, HintType.Info, False)
|
||
#End If
|
||
Case LogLevel.Hint
|
||
Hint(Text, HintType.Critical, False)
|
||
Case LogLevel.Msgbox
|
||
MyMsgBox(Text, Title, IsWarn:=True)
|
||
Case LogLevel.Feedback
|
||
If CanFeedback(False) Then
|
||
If MyMsgBox(Text & vbCrLf & vbCrLf & "是否反馈此问题?如果不反馈,这个问题可能永远无法得到解决!", Title, "反馈", "取消", IsWarn:=True) = 1 Then Feedback(False, True)
|
||
Else
|
||
MyMsgBox(Text & vbCrLf & vbCrLf & "将 PCL 更新至最新版或许可以解决这个问题……", Title, IsWarn:=True)
|
||
End If
|
||
Case LogLevel.Critical
|
||
If IsCriticalErrorTriggered Then
|
||
FormMain.EndProgramForce(ProcessReturnValues.Exception)
|
||
Return
|
||
End If
|
||
IsCriticalErrorTriggered = True
|
||
If CanFeedback(False) Then
|
||
If MsgBox(Text & vbCrLf & vbCrLf & "是否反馈此问题?如果不反馈,这个问题可能永远无法得到解决!", MsgBoxStyle.Critical + MsgBoxStyle.YesNo, Title) = MsgBoxResult.Yes Then Feedback(False, True)
|
||
Else
|
||
MsgBox(Text & vbCrLf & vbCrLf & "将 PCL 更新至最新版或许可以解决这个问题……", MsgBoxStyle.Critical, Title)
|
||
End If
|
||
End Select
|
||
|
||
End Sub
|
||
''' <summary>
|
||
''' 输出错误信息。
|
||
''' </summary>
|
||
''' <param name="Desc">错误描述。会在处理时在末尾加入冒号。</param>
|
||
Public Sub Log(Ex As Exception, Desc As String, Optional Level As LogLevel = LogLevel.Debug, Optional Title As String = "出现错误")
|
||
'On Error Resume Next
|
||
If TypeOf Ex Is ThreadInterruptedException Then Return
|
||
|
||
'获取错误信息
|
||
Dim ExFull As String = Desc & ":" & Ex.Message
|
||
|
||
'输出日志
|
||
If {LogLevel.Msgbox, LogLevel.Hint}.Contains(Level) Then
|
||
LogWrapper.Warn(Ex, Desc)
|
||
ElseIf LogLevel.Feedback = Level Then
|
||
LogWrapper.Error(Ex, Desc)
|
||
ElseIf LogLevel.Critical = Level Then
|
||
LogWrapper.Fatal(Ex, Desc)
|
||
ElseIf LogLevel.Debug = Level Then
|
||
LogWrapper.Debug($"{Desc}:{Ex}")
|
||
ElseIf LogLevel.Developer = Level Then
|
||
LogWrapper.Trace($"{Desc}:{Ex}")
|
||
Else
|
||
LogWrapper.Error(Ex, Desc)
|
||
End If
|
||
|
||
If IsProgramEnded Then Return
|
||
|
||
If Ex.GetType() = GetType(ComponentModel.Win32Exception) Then ExFull += vbCrLf & "与系统底层交互失败,请尝试重新安装 .NET 8 解决此问题"
|
||
|
||
'输出提示
|
||
Select Case Level
|
||
Case LogLevel.Normal
|
||
#If DEBUGRESERVED Then
|
||
Case LogLevel.Developer
|
||
Dim ExLine As String = Desc & ":" & Ex.ToString()
|
||
Hint("[开发者模式] " & ExLine, HintType.Info, False)
|
||
Case LogLevel.Debug
|
||
Dim ExLine As String = Desc & ":" & Ex.ToString()
|
||
Hint("[调试模式] " & ExLine, HintType.Info, False)
|
||
#Else
|
||
Case LogLevel.Developer
|
||
Case LogLevel.Debug
|
||
Dim ExLine As String = Desc & ":" & Ex.ToString()
|
||
If ModeDebug Then Hint("[调试模式] " & ExLine, HintType.Info, False)
|
||
#End If
|
||
Case LogLevel.Hint
|
||
Dim ExLine As String = Desc & ":" & Ex.ToString()
|
||
Hint(ExLine, HintType.Critical, False)
|
||
Case LogLevel.Msgbox
|
||
MyMsgBox(ExFull, Title, IsWarn:=True)
|
||
Case LogLevel.Feedback
|
||
If CanFeedback(False) Then
|
||
If MyMsgBox(ExFull & vbCrLf & vbCrLf & "是否反馈此问题?如果不反馈,这个问题可能永远无法得到解决!", Title, "反馈", "取消", IsWarn:=True) = 1 Then Feedback(False, True)
|
||
Else
|
||
MyMsgBox(ExFull & vbCrLf & vbCrLf & "将 PCL 更新至最新版或许可以解决这个问题……", Title, IsWarn:=True)
|
||
End If
|
||
Case LogLevel.Critical
|
||
If IsCriticalErrorTriggered Then
|
||
FormMain.EndProgramForce(ProcessReturnValues.Exception)
|
||
Return
|
||
End If
|
||
IsCriticalErrorTriggered = True
|
||
If CanFeedback(False) Then
|
||
If MsgBox(ExFull & vbCrLf & vbCrLf & "是否反馈此问题?如果不反馈,这个问题可能永远无法得到解决!", MsgBoxStyle.Critical + MsgBoxStyle.YesNo, Title) = MsgBoxResult.Yes Then Feedback(False, True)
|
||
Else
|
||
MsgBox(ExFull & vbCrLf & vbCrLf & "将 PCL 更新至最新版或许可以解决这个问题……", MsgBoxStyle.Critical, Title)
|
||
End If
|
||
End Select
|
||
|
||
End Sub
|
||
Public Function Base64Decode(Text As String) As String
|
||
If String.IsNullOrWhiteSpace(Text) Then Return ""
|
||
Dim decodedBytes As Byte() = Convert.FromBase64String(Text)
|
||
Return System.Text.Encoding.UTF8.GetString(decodedBytes)
|
||
End Function
|
||
Public Function Base64Encode(Text As String) As String
|
||
Dim bytes As Byte() = System.Text.Encoding.UTF8.GetBytes(Text)
|
||
Return Convert.ToBase64String(bytes)
|
||
End Function
|
||
Public Function Base64Encode(bytes As Byte()) As String
|
||
Return Convert.ToBase64String(bytes)
|
||
End Function
|
||
'反馈
|
||
Public Sub Feedback(Optional ShowMsgbox As Boolean = True, Optional ForceOpenLog As Boolean = False)
|
||
'On Error Resume Next
|
||
FeedbackInfo()
|
||
Dim currentDate As String
|
||
currentDate = Format(Now, "yyyy-M-dd")
|
||
|
||
If ForceOpenLog OrElse (ShowMsgbox AndAlso MyMsgBox("若你在汇报一个 Bug,请点击 打开文件夹 按钮,并上传 Launch-" & currentDate & "-[一串数字].log 中包含错误信息的文件。" & vbCrLf & "游戏崩溃一般与启动器无关,请不要因为游戏崩溃而提交反馈。", "反馈提交提醒", "打开文件夹", "不需要") = 1) Then
|
||
OpenExplorer(ExePath & "PCL\Log\")
|
||
End If
|
||
OpenWebsite("https://github.com/PCL-Community/PCL2-CE/issues/")
|
||
End Sub
|
||
Public Function CanFeedback(ShowHint As Boolean) As Boolean
|
||
If Not IsVerisonLatest() Then
|
||
If ShowHint Then
|
||
If MyMsgBox($"你的 PCL 不是最新版,因此无法提交反馈。{vbCrLf}请在更新后,确认该问题在最新版中依然存在,然后再提交反馈。", "无法提交反馈", "更新", "取消") = 1 Then
|
||
UpdateCheckByButton()
|
||
End If
|
||
End If
|
||
Return False
|
||
Else
|
||
Return True
|
||
End If
|
||
End Function
|
||
''' <summary>
|
||
''' 在日志中输出系统诊断信息。
|
||
''' </summary>
|
||
Public Sub FeedbackInfo()
|
||
'On Error Resume Next
|
||
Dim phyRam = KernelInterop.GetPhysicalMemoryBytes()
|
||
Log("[System] 诊断信息:" & vbCrLf &
|
||
"操作系统:" & RuntimeInformation.OSDescription & "(32 位:" & Is32BitSystem & ")" & vbCrLf &
|
||
"剩余内存:" & Int(phyRam.Available / 1024 / 1024) & " M / " & Int(phyRam.Total / 1024 / 1024) & " M" & vbCrLf &
|
||
"DPI:" & DPI & "(" & Math.Round(DPI / 96, 2) * 100 & "%)" & vbCrLf &
|
||
"MC 文件夹:" & If(PathMcFolder, "Nothing") & vbCrLf &
|
||
"文件位置:" & ExePath)
|
||
End Sub
|
||
|
||
'断言
|
||
Public Sub DebugAssert(Exp As Boolean)
|
||
If Not Exp Then Throw New Exception("断言命中")
|
||
End Sub
|
||
|
||
'获取当前的堆栈信息
|
||
Public Function GetStackTrace() As String
|
||
Dim Stack As New StackTrace()
|
||
Return Join(Stack.GetFrames().Skip(1).Select(Function(f) f.GetMethod).
|
||
Select(Function(f) f.Name & "(" & Join(f.GetParameters.Select(Function(p) p.ToString).ToList, ", ") & ") - " & f.Module.ToString).ToList,
|
||
vbCrLf).Replace(vbCrLf & vbCrLf, vbCrLf)
|
||
End Function
|
||
|
||
#End Region
|
||
|
||
End Module
|
||
|
||
#Region "WPF"
|
||
|
||
''' <summary>
|
||
''' 对数据绑定进行加法运算,使用参数决定加数。
|
||
''' </summary>
|
||
Public Class AdditionConverter
|
||
Implements IValueConverter
|
||
Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
|
||
If value Is Nothing Then Return 0
|
||
Dim before As Double
|
||
If Not Double.TryParse(value.ToString(), before) Then Return 0
|
||
Dim scale As Double = 1
|
||
If parameter IsNot Nothing Then Double.TryParse(parameter.ToString(), scale)
|
||
Return before + scale
|
||
End Function
|
||
Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
|
||
If value Is Nothing Then Return Binding.DoNothing
|
||
Dim before As Double
|
||
If Not Double.TryParse(value.ToString(), before) Then Return Binding.DoNothing
|
||
Dim scale As Double = 1
|
||
If parameter IsNot Nothing Then Double.TryParse(parameter.ToString(), scale)
|
||
If scale = 0 Then Return Binding.DoNothing
|
||
Return before - scale
|
||
End Function
|
||
End Class
|
||
|
||
''' <summary>
|
||
''' 对数据绑定进行乘法运算,使用参数决定乘数。
|
||
''' </summary>
|
||
Public Class MultiplicationConverter
|
||
Implements IValueConverter
|
||
Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
|
||
If value Is Nothing Then Return 0
|
||
Dim before As Double
|
||
If Not Double.TryParse(value.ToString(), before) Then Return 0
|
||
Dim scale As Double = 1
|
||
If parameter IsNot Nothing Then Double.TryParse(parameter.ToString(), scale)
|
||
Return before * scale
|
||
End Function
|
||
Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
|
||
If value Is Nothing Then Return Binding.DoNothing
|
||
Dim before As Double
|
||
If Not Double.TryParse(value.ToString(), before) Then Return Binding.DoNothing
|
||
Dim scale As Double = 1
|
||
If parameter IsNot Nothing Then Double.TryParse(parameter.ToString(), scale)
|
||
If scale = 0 Then Return Binding.DoNothing
|
||
Return before / scale
|
||
End Function
|
||
End Class
|
||
|
||
''' <summary>
|
||
''' 将取反的 Boolean 绑定到 Visibility。
|
||
''' </summary>
|
||
Public Class InverseBooleanToVisibilityConverter
|
||
Implements IValueConverter
|
||
Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
|
||
If value Is Nothing Then Return Visibility.Visible
|
||
Dim boolValue As Boolean
|
||
Return If(Boolean.TryParse(value.ToString(), boolValue), If(boolValue, Visibility.Collapsed, Visibility.Visible), Visibility.Visible)
|
||
End Function
|
||
Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
|
||
If value Is Nothing Then Return False
|
||
Return If(TypeOf value Is Visibility, value <> Visibility.Visible, False)
|
||
End Function
|
||
End Class
|
||
|
||
''' <summary>
|
||
''' 将 Boolean 取反。
|
||
''' </summary>
|
||
Public Class InverseBooleanConverter
|
||
Implements IValueConverter
|
||
Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
|
||
If value Is Nothing Then Return False
|
||
Dim boolValue As Boolean
|
||
Return If(Boolean.TryParse(value.ToString(), boolValue), Not boolValue, False)
|
||
End Function
|
||
Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
|
||
If value Is Nothing Then Return False
|
||
Return If(Boolean.TryParse(value.ToString(), value), Not value, False)
|
||
End Function
|
||
End Class
|
||
|
||
#End Region
|