This commit is contained in:
龙腾猫跃
2025-10-07 04:57:14 +08:00
parent 86ed0e9ae0
commit a41579b1da
71 changed files with 1151 additions and 1340 deletions
+295 -207
View File
@@ -1,181 +1,291 @@
Imports System.Collections.ObjectModel
#Region "附加属性"
''' <summary>
''' 用于在 XAML 中初始化列表对象。
''' 附加属性无法在 XAML 中为每个对象初始化独立的列表对象,因此需要一个包装类,然后在 XAML 中显式初始化。
''' </summary>
<Markup.ContentProperty("Events")>
Public Class CustomEventCollection
Implements IEnumerable(Of CustomEvent)
Dim _Events As New List(Of CustomEvent)
Public ReadOnly Property Events As List(Of CustomEvent)
Get
Return _Events
End Get
End Property
Public Function GetEnumerator() As IEnumerator(Of CustomEvent) Implements IEnumerable(Of CustomEvent).GetEnumerator
Return DirectCast(Events, IEnumerable(Of CustomEvent)).GetEnumerator()
End Function
Private Function IEnumerable_GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
Return DirectCast(Events, IEnumerable).GetEnumerator()
End Function
End Class
''' <summary>
''' 提供自定义事件的附加属性。
''' </summary>
Public Class CustomEventService
'Events
Public Shared ReadOnly EventsProperty As DependencyProperty =
DependencyProperty.RegisterAttached("Events", GetType(CustomEventCollection), GetType(CustomEventService), New PropertyMetadata(Nothing))
<AttachedPropertyBrowsableForType(GetType(DependencyObject))>
Public Shared Sub SetEvents(d As DependencyObject, value As CustomEventCollection)
d.SetValue(EventsProperty, value)
End Sub
<AttachedPropertyBrowsableForType(GetType(DependencyObject))>
Public Shared Function GetEvents(d As DependencyObject) As CustomEventCollection
If d.GetValue(EventsProperty) Is Nothing Then d.SetValue(EventsProperty, New CustomEventCollection)
Return d.GetValue(EventsProperty)
End Function
'EventType
Public Shared ReadOnly EventTypeProperty As DependencyProperty =
DependencyProperty.RegisterAttached("EventType", GetType(CustomEvent.EventType), GetType(CustomEventService), New PropertyMetadata(Nothing))
<AttachedPropertyBrowsableForType(GetType(DependencyObject))>
Public Shared Sub SetEventType(d As DependencyObject, value As CustomEvent.EventType)
d.SetValue(EventTypeProperty, value)
End Sub
<AttachedPropertyBrowsableForType(GetType(DependencyObject))>
Public Shared Function GetEventType(d As DependencyObject) As CustomEvent.EventType
Return d.GetValue(EventTypeProperty)
End Function
'EventData
Public Shared ReadOnly EventDataProperty As DependencyProperty =
DependencyProperty.RegisterAttached("EventData", GetType(String), GetType(CustomEventService), New PropertyMetadata(Nothing))
<AttachedPropertyBrowsableForType(GetType(DependencyObject))>
Public Shared Sub SetEventData(d As DependencyObject, value As String)
d.SetValue(EventDataProperty, value)
End Sub
<AttachedPropertyBrowsableForType(GetType(DependencyObject))>
Public Shared Function GetEventData(d As DependencyObject) As String
Return d.GetValue(EventDataProperty)
End Function
End Class
Partial Public Module ModMain
Public Module ModEvent
#Region "事件执行方法"
''' <summary>
''' 供外部调用的自定义事件执行接口,会在新建工作线程中运行逻辑。
''' 触发该控件上的自定义事件。
''' 事件会在新线程中执行。
''' </summary>
''' <param name="SingleEventType">xaml中的EventType</param>
''' <param name="SingleEventData">xaml中的EventData</param>
''' <param name="EventCollection">xaml中的CustomEvents</param>
Public Sub ProcessCustomEvents(SingleEventType As String, SingleEventData As String, EventCollection As CustomEventCollection)
<Runtime.CompilerServices.Extension>
Public Sub RaiseCustomEvent(Control As DependencyObject)
'收集事件列表
Dim Events = CustomEventService.GetEvents(Control).ToList
Dim EventType = CustomEventService.GetEventType(Control)
If EventType <> CustomEvent.EventType.None Then Events.Add(New CustomEvent(EventType, CustomEventService.GetEventData(Control)))
'执行事件
If Not Events.Any Then Return
RunInNewThread(
Sub()
Try
'先执行单个事件
StartCustomEvent(SingleEventType, SingleEventData)
'再挨个执行事件集合中的事件
If EventCollection IsNot Nothing Then
For Each AnEvent In RunInUiWait(Function() EventCollection.DeepCloneToStructureList())
StartCustomEvent(AnEvent.EventType, AnEvent.EventData)
Next
End If
Catch ex As Exception
Log(ex, "自定义事件执行器发生异常", LogLevel.Feedback)
End Try
End Sub, "CustomEventProcessor " & GetUuid())
End Sub
''' <summary>
''' 对当前线程阻塞地调用自定义事件处理器,接受来自 xaml 原始输入作为参数。
''' </summary>
Private Sub StartCustomEvent(Type As String, DataRaw As String)
If String.IsNullOrEmpty(Type) Then Exit Sub
Dim Data = If(DataRaw Is Nothing, {""}, DataRaw.Split("|"))
Log("[Control] 执行自定义事件:" & Type & ", " & Join(Data, ", "))
'检测是否存在对应事件
If Not CustomEventProcessorsDict.ContainsKey(Type) Then
MyMsgBox($"未知的事件类型:'{Type}'{vbCrLf}请检查事件类型填写是否正确,或者 PCL 是否为最新版本。", "事件执行失败", ForceWait:=True)
Exit Sub
End If
'调用事件处理器
Try
CustomEventProcessorsDict(Type)(Data)
Catch ex As Exception
Log(ex, $"自定义事件执行失败:{Type}", LogLevel.Msgbox, "自定义主页事件执行失败")
End Try
Sub()
For Each e In Events
e.Raise()
Next
End Sub, "执行自定义事件 " & GetUuid())
End Sub
''' <summary>
''' 存储各个自定义事件处理器的字典,以 EventType 为键,执行自定义事件时在工作线程中调用 Action 并传入分割后的 EventData 。
''' Action 应当在事件逻辑结束后,即可以开始下一个事件时再结束;以便于挨个调用事件不打架。
''' </summary>
Private ReadOnly CustomEventProcessorsDict As New Dictionary(Of String, Action(Of String())) From {
{"打开网页",
Sub(Data As String())
Data(0) = Data(0).Replace("\", "/")
If Not Data(0).Contains("://") OrElse Data(0).StartsWithF("file", True) Then '为了支持更多协议(#2200)
MyMsgBox("打开网页事件中,EventData 必须为一个网址。" & vbCrLf & "如果想要启动程序,请将 EventType 改为 打开文件。", "事件执行失败")
Exit Sub
End If
Hint("网页正在开启中,请稍候……")
OpenWebsite(Data(0))
End Sub},
{"打开文件",
Sub(Data As String())
Dim ActualPaths = GetEventAbsoluteUrls(Data(0), "打开文件")
Dim Location = ActualPaths(0), WorkingDir = ActualPaths(1)
Log($"[Control] 打开文件:路径:{Location},工作目录:{WorkingDir}")
If Not CommandRunUserConfirm(If(Data.Length < 2, Location, $"{Location} {Data(1)}")) Then Exit Sub
Dim Info As New ProcessStartInfo With {
.Arguments = If(Data.Length < 2, "", Data(1)),
.FileName = Location,
.WorkingDirectory = ShortenPath(WorkingDir)
}
Process.Start(Info)
End Sub},
{"打开帮助",
Sub(Data As String())
Dim ActualPaths = GetEventAbsoluteUrls(Data(0), "打开帮助")
Dim Location = ActualPaths(0)
Log($"[Control] 打开帮助:{Location}")
PageOtherHelp.EnterHelpPage(Location)
End Sub},
{"执行命令",
Sub(Data As String())
Dim ActualPaths = GetEventAbsoluteUrls(Data(0), "执行命令")
Dim Location = ActualPaths(0), WorkingDir = ActualPaths(1)
Log($"[Control] 执行命令:路径:{Location},工作目录:{WorkingDir}")
If Not CommandRunUserConfirm(If(Data.Length < 2, Location, $"{Location} {Data(1)}")) Then Exit Sub
Dim Info As New ProcessStartInfo With {
.Arguments = If(Data.Length < 2, "", Data(1)),
.FileName = Location,
.WorkingDirectory = ShortenPath(WorkingDir)
}
Process.Start(Info)
End Sub},
{"启动游戏",
Sub(Data As String())
If Data(0) = "\current" Then
If McVersionCurrent Is Nothing Then
Hint("请先选择一个 Minecraft 版本!", HintType.Critical)
Exit Sub
Else
Data(0) = McVersionCurrent.Name
End If
End If
RunInUiWait(
Sub()
If McLaunchStart(New McLaunchOptions With {.Version = New McVersion(Data(0)), .ServerIp = If(Data.Length >= 2, Data(1), Nothing)}) Then
Hint("正在启动 " & Data(0) & "……")
End If
End Sub)
End Sub},
{"复制文本", Sub(Data As String()) ClipboardSet(Join(Data, "|"))},
{"刷新主页",
Sub(Data As String())
RunInUiWait(Sub() FrmLaunchRight.ForceRefresh())
If Data(0) = "" Then Hint("已刷新主页!", HintType.Finish)
End Sub},
{"刷新帮助", Sub() RunInUi(Sub() PageOtherLeft.RefreshHelp())},
{"今日人品", Sub() PageOtherTest.Jrrp()},
{"内存优化",
Sub(Data As String())
If Data.Length >= 1 AndAlso Data(0) = "前台运行" Then
PageOtherTest.MemoryOptimize(Data.Length >= 2 AndAlso Data(1) = "显示提示")
Else
RunInNewThread(Sub() PageOtherTest.MemoryOptimize(True))
End If
End Sub},
{"弹出窗口", Sub(Data As String()) MyMsgBox(Data(1).Replace("\n", vbCrLf), Data(0).Replace("\n", vbCrLf), ForceWait:=True)},
{"切换页面", Sub(Data As String()) RunInUiWait(Sub() FrmMain.PageChange(Val(Data(0)), Val(Data(1))))},
{"安装整合包", Sub() RunInUiWait(Sub() ModpackInstall())},
{"导入整合包", Sub() RunInUiWait(Sub() ModpackInstall())},
{"下载文件",
Sub(Data As String())
Data(0) = Data(0).Replace("\", "/")
If Not (Data(0).StartsWithF("http://", True) OrElse Data(0).StartsWithF("https://", True)) Then
MyMsgBox("EventData 必须为以 http:// 或 https:// 开头的网址。" & vbCrLf & "PCL 不支持其他乱七八糟的下载协议。", "事件执行失败")
Exit Sub
End If
Try
Select Case Data.Length
Case 1
PageOtherTest.StartCustomDownload(Data(0), GetFileNameFromPath(Data(0)))
Case 2
PageOtherTest.StartCustomDownload(Data(0), Data(1))
Case Else
PageOtherTest.StartCustomDownload(Data(0), Data(1), Data(2))
End Select
Catch
PageOtherTest.StartCustomDownload(Data(0), "未知")
End Try
End Sub}
}
End Module
#End Region
#Region "工具方法"
''' <summary>
''' 自定义事件。
''' </summary>
Public Class CustomEvent
#Region "属性与触发"
Public Property Type As EventType = EventType.None
Public Property Data As String = Nothing
Public Sub New()
End Sub
Public Sub New(Type As EventType, Data As String)
Me.Type = Type
Me.Data = Data
End Sub
''' <summary>
''' 弹窗询问用户是否同意继续执行打开类事件。
''' 在当前线程中触发该自定义事件。
''' </summary>
Private Function CommandRunUserConfirm(TextShowToUser As String) As Boolean
If Setup.Get("HintCustomCommand") Then Return True
Select Case MyMsgBox("即将执行:" & TextShowToUser & vbCrLf & "请在确认该操作没有安全隐患后继续。", "执行确认", "继续", "继续且今后不再要求确认", "取消")
Case 1
Return True
Case 2
Setup.Set("HintCustomCommand", True)
Return True
Case Else '3
Log("[Control] 用户取消了打开类事件")
Return False
End Select
End Function
Public Sub Raise()
Raise(Type, Data)
End Sub
#End Region
Public Enum EventType
None = 0
打开网页
打开文件
打开帮助
执行命令
启动游戏
复制文本
刷新主页
刷新页面
刷新帮助
今日人品
内存优化
清理垃圾
弹出窗口
弹出提示
切换页面
导入整合包
安装整合包
下载文件
修改设置
写入设置
修改变量
写入变量
End Enum
''' <summary>
''' 在当前线程中触发一个自定义事件。
''' </summary>
Public Shared Sub Raise(Type As EventType, Arg As String)
If Type = EventType.None Then Return
Log($"[Control] 执行自定义事件:{Type}, {Arg}")
Try
Dim Args As String() = If(Arg?.Split("|"), {""})
Select Case Type
Case EventType.打开网页
Arg = Arg.Replace("\", "/")
If Not Arg.Contains("://") OrElse Arg.StartsWithF("file", True) Then '为了支持更多协议(#2200)
MyMsgBox("EventData 必须为一个网址。" & vbCrLf & "如果想要启动程序,请将 EventType 改为 打开文件。", "事件执行失败")
Return
End If
Hint("正在开启中,请稍候:" & Arg)
RunInThread(Sub() OpenWebsite(Arg))
Case EventType.打开文件, EventType.打开帮助, EventType.执行命令
RunInThread(
Sub()
Try
'确认实际路径
Dim ActualPaths = GetAbsoluteUrls(Args(0), Type)
Dim Location = ActualPaths(0), WorkingDir = ActualPaths(1)
Log($"[Control] 打开类自定义事件实际路径:{Location},工作目录:{WorkingDir}")
'执行
If Type = EventType.打开帮助 Then
PageOtherHelp.EnterHelpPage(Location)
Else
If Not EventSafetyConfirm("即将执行:" & Location & If(Args.Length >= 2, " " & Args(1), "")) Then Return
Dim Info As New ProcessStartInfo With {
.Arguments = If(Args.Length >= 2, Args(1), ""),
.FileName = Location,
.WorkingDirectory = WorkingDir
}
StartProcess(Info)
End If
Catch ex As Exception
Log(ex, "执行打开类自定义事件失败", LogLevel.Msgbox)
End Try
End Sub)
Case EventType.启动游戏
If Args(0) = "\current" Then
If McVersionCurrent Is Nothing Then
Hint("请先选择一个 Minecraft 版本!", HintType.Red)
Return
Else
Args(0) = McVersionCurrent.Name
End If
End If
RunInUi(
Sub()
If McLaunchStart(New McLaunchOptions With
{.ServerIp = If(Args.Length >= 2, Args(1), Nothing), .Version = New McVersion(Args(0))}) Then
Hint($"正在启动 {Args(0)}……")
End If
End Sub)
Case EventType.复制文本
ClipboardSet(Arg)
Case EventType.刷新主页, EventType.刷新页面
If TypeOf FrmMain.PageRight Is IRefreshable Then
RunInUiWait(Sub() CType(FrmMain.PageRight, IRefreshable).Refresh())
If String.IsNullOrEmpty(Arg) Then Hint("已刷新!", HintType.Green)
Else
Hint("当前页面不支持刷新操作!", HintType.Red)
End If
Case EventType.刷新帮助
RunInUiWait(Sub() PageOtherLeft.RefreshHelp())
If String.IsNullOrEmpty(Arg) Then Hint("已刷新!", HintType.Green)
Case EventType.今日人品
PageOtherTest.Jrrp()
Case EventType.内存优化
RunInThread(Sub() PageOtherTest.MemoryOptimize(True))
Case EventType.清理垃圾
RunInThread(Sub() PageOtherTest.RubbishClear())
Case EventType.弹出窗口
If Args.Length = 1 Then Throw New Exception($"EventType {Type} 需要至少 2 个以 | 分割的参数,例如 弹窗标题|弹窗内容")
MyMsgBox(Args(1).Replace("\n", vbCrLf), Args(0).Replace("\n", vbCrLf), If(Args.Length > 2, Args(2), "确定"))
Case EventType.弹出提示
Hint(Args(0).Replace("\n", vbCrLf), If(Args.Length = 1, HintType.Blue, Args(1).ParseToEnum(Of HintType)))
Case EventType.切换页面
RunInUi(Sub() FrmMain.PageChange(
Args(0).ParseToEnum(Of FormMain.PageType),
If(Args.Length = 1, FormMain.PageSubType.Default, Args(1).ParseToEnum(Of FormMain.PageSubType))))
Case EventType.导入整合包, EventType.安装整合包
RunInUi(Sub() ModpackInstall())
Case EventType.下载文件
Args(0) = Args(0).Replace("\", "/")
If Not (Args(0).StartsWithF("http://", True) OrElse Args(0).StartsWithF("https://", True)) Then
MyMsgBox("EventData 必须为以 http:// 或 https:// 开头的网址。" & vbCrLf & "PCL 不支持其他乱七八糟的下载协议。", "事件执行失败")
Return
End If
If Not EventSafetyConfirm("即将从该网址下载文件:" & vbCrLf & Args(0)) Then Return
Try
Select Case Args.Length
Case 1
PageOtherTest.StartCustomDownload(Args(0), GetFileNameFromPath(Args(0)))
Case 2
PageOtherTest.StartCustomDownload(Args(0), Args(1))
Case Else
PageOtherTest.StartCustomDownload(Args(0), Args(1), Args(2))
End Select
Catch
PageOtherTest.StartCustomDownload(Args(0), "未知")
End Try
Case EventType.修改设置, EventType.写入设置
If Args.Length = 1 Then Throw New Exception($"EventType {Type} 需要至少 2 个以 | 分割的参数,例如 UiLauncherTransparent|400")
Setup.SetSafe(Args(0), Args(1), Version:=McVersionCurrent)
If Args.Length = 2 Then Hint($"已写入设置:{Args(0)} → {Args(1)}", HintType.Green)
Case EventType.修改变量, EventType.写入变量
If Args.Length = 1 Then Throw New Exception($"EventType {Type} 需要至少 2 个以 | 分割的参数,例如 VariableName|SomeValue")
WriteReg("CustomEvent" & Args(0), Args(1))
If Args.Length = 2 Then Hint($"已写入变量:{Args(0)} → {Args(1)}", HintType.Green)
Case Else
MyMsgBox("未知的事件类型:" & Type & vbCrLf & "请检查事件类型填写是否正确,或者 PCL 是否为最新版本。", "事件执行失败")
End Select
Catch ex As Exception
Log(ex, $"事件执行失败({Type}, {Arg})", LogLevel.Msgbox)
End Try
End Sub
''' <summary>
''' 返回自定义事件的绝对 Url。实际返回 {绝对 Url, WorkingDir}。
''' 失败会抛出异常。
''' </summary>
Public Function GetEventAbsoluteUrls(RelativeUrl As String, EventType As String) As String()
Public Shared Function GetAbsoluteUrls(RelativeUrl As String, Type As EventType) As String()
'网页确认
If RelativeUrl.StartsWithF("http", True) Then
@@ -216,25 +326,25 @@ Public Module ModEvent
If RelativeUrl.Contains(":\") Then
'绝对路径
Location = RelativeUrl
Log("[Control] 自定义事件中由绝对路径" & EventType & ":" & Location)
Log("[Control] 自定义事件中由绝对路径" & Type & ":" & Location)
ElseIf File.Exists(Path & "PCL\" & RelativeUrl) Then
'相对 PCL 文件夹的路径
Location = Path & "PCL\" & RelativeUrl
Log("[Control] 自定义事件中由相对 PCL 文件夹的路径" & EventType & ":" & Location)
Log("[Control] 自定义事件中由相对 PCL 文件夹的路径" & Type & ":" & Location)
ElseIf File.Exists(Path & "PCL\Help\" & RelativeUrl) Then
'相对 PCL 本地帮助文件夹的路径
Location = Path & "PCL\Help\" & RelativeUrl
WorkingDir = Path & "PCL\Help\"
Log("[Control] 自定义事件中由相对 PCL 本地帮助文件夹的路径" & EventType & ":" & Location)
ElseIf EventType = "打开帮助" AndAlso File.Exists(PathTemp & "Help\" & RelativeUrl) Then
Log("[Control] 自定义事件中由相对 PCL 本地帮助文件夹的路径" & Type & ":" & Location)
ElseIf Type = EventType.打开帮助 AndAlso File.Exists(PathTemp & "Help\" & RelativeUrl) Then
'相对 PCL 自带帮助文件夹的路径
Location = PathTemp & "Help\" & RelativeUrl
WorkingDir = PathTemp & "Help\"
Log("[Control] 自定义事件中由相对 PCL 自带帮助文件夹的路径" & EventType & ":" & Location)
ElseIf EventType = "打开文件" OrElse EventType = "执行命令" Then
Log("[Control] 自定义事件中由相对 PCL 自带帮助文件夹的路径" & Type & ":" & Location)
ElseIf Type = EventType.打开文件 OrElse Type = EventType.执行命令 Then
'直接使用原有路径启动程序
Location = RelativeUrl
Log("[Control] 自定义事件中直接" & EventType & ":" & Location)
Log("[Control] 自定义事件中直接" & Type & ":" & Location)
Else
'打开帮助,但是格式不对劲
Throw New FileNotFoundException("未找到 EventData 指向的本地 xaml 文件:" & RelativeUrl, RelativeUrl)
@@ -243,42 +353,20 @@ Public Module ModEvent
Return {Location, WorkingDir}
End Function
#End Region
End Module
#Region "数据结构类声明"
Public Class CustomEventCollection
Inherits ObservableCollection(Of CustomEvent)
Public Function DeepCloneToStructureList() As IList(Of CustomEventStructure)
DeepCloneToStructureList = New List(Of CustomEventStructure)
For Each AnEvent In Me
DeepCloneToStructureList.Add(New CustomEventStructure With {.EventType = AnEvent.EventType, .EventData = AnEvent.EventData})
Next
''' <summary>
''' 弹出安全确认弹窗。返回是否继续执行。
''' </summary>
Private Shared Function EventSafetyConfirm(Message As String) As Boolean
If Setup.Get("HintCustomCommand") Then Return True
Select Case MyMsgBox(Message & vbCrLf & "请在确认没有安全隐患后再继续。", "执行确认", "继续", "继续且今后不再要求确认", "取消")
Case 1
Return True
Case 2
Setup.Set("HintCustomCommand", True)
Return True
Case Else
Return False
End Select
End Function
End Class
Public Class CustomEvent
Inherits DependencyObject
Public Property EventType As String
Get
Return GetValue(EventTypeProperty)
End Get
Set(value As String)
SetValue(EventTypeProperty, value)
End Set
End Property
Public Shared ReadOnly EventTypeProperty As DependencyProperty = DependencyProperty.Register("EventType", GetType(String), GetType(CustomEvent), New PropertyMetadata(Nothing))
Public Property EventData As String
Get
Return GetValue(EventDataProperty)
End Get
Set(value As String)
SetValue(EventDataProperty, value)
End Set
End Property
Public Shared ReadOnly EventDataProperty As DependencyProperty = DependencyProperty.Register("EventData", GetType(String), GetType(CustomEvent), New PropertyMetadata(Nothing))
End Class
Public Structure CustomEventStructure
Public EventType As String
Public EventData As String
End Structure
#End Region
End Class