ClipboardInterceptor.vb 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. ' Author: uye (owner of the MaaAssistantArknights team)
  2. ' Original Source: MaaAssistantArknights project - https://github.com/MaaAssistantArknights/MaaAssistantArknights
  3. ' License: Apache License 2.0 (this file only)
  4. '
  5. ' This file is originally developed for MaaAssistantArknights and licensed under AGPL v3.0.
  6. ' As the original author and copyright holder,
  7. ' I hereby re-license this file under the Apache License 2.0 for use in the Plain Craft Launcher project.
  8. '
  9. ' This file is contributed under the spirit of good faith and open cooperation.
  10. ' It does not implement any core launcher functions of PCL.
  11. '
  12. ' Description:
  13. ' Provides a WPF clipboard handling fix to avoid OpenClipboard exceptions
  14. ' in TextBox, RichTextBox, and DataGrid under Windows focus issues or clipboard hooks.
  15. '
  16. ' Date: 2025-07-03
  17. Namespace Controls.Behaviors
  18. Public NotInheritable Class ClipboardInterceptor
  19. Private Sub New()
  20. End Sub
  21. Public Shared ReadOnly EnableSafeClipboardProperty As DependencyProperty =
  22. DependencyProperty.RegisterAttached("EnableSafeClipboard", GetType(Boolean), GetType(ClipboardInterceptor),
  23. New PropertyMetadata(False, AddressOf OnEnableSafeClipboardChanged))
  24. Public Shared Sub SetEnableSafeClipboard(element As DependencyObject, value As Boolean)
  25. element.SetValue(EnableSafeClipboardProperty, value)
  26. End Sub
  27. Public Shared Function GetEnableSafeClipboard(element As DependencyObject) As Boolean
  28. Return element.GetValue(EnableSafeClipboardProperty)
  29. End Function
  30. Private Shared Sub OnEnableSafeClipboardChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
  31. If TypeOf d Is TextBox AndAlso CBool(e.NewValue) Then
  32. AddCommandBindingsToTextBox(DirectCast(d, TextBox))
  33. ElseIf TypeOf d Is RichTextBox AndAlso CBool(e.NewValue) Then
  34. AddCommandBindingsToRichTextBox(DirectCast(d, RichTextBox))
  35. ElseIf TypeOf d Is DataGrid AndAlso CBool(e.NewValue) Then
  36. AddCommandBindingsToDataGrid(DirectCast(d, DataGrid))
  37. End If
  38. End Sub
  39. Private Shared Sub AddCommandBindingsToTextBox(tb As TextBox)
  40. tb.CommandBindings.Add(New CommandBinding(ApplicationCommands.Copy, AddressOf OnCopyTextBox))
  41. tb.CommandBindings.Add(New CommandBinding(ApplicationCommands.Cut, AddressOf OnCutTextBox, AddressOf CanEditTextBox))
  42. tb.CommandBindings.Add(New CommandBinding(ApplicationCommands.Paste, AddressOf OnPasteTextBox, AddressOf CanEditTextBox))
  43. End Sub
  44. Private Shared Sub AddCommandBindingsToRichTextBox(rtb As RichTextBox)
  45. rtb.CommandBindings.Add(New CommandBinding(ApplicationCommands.Copy, AddressOf OnCopyRichTextBox))
  46. rtb.CommandBindings.Add(New CommandBinding(ApplicationCommands.Cut, AddressOf OnCutRichTextBox))
  47. rtb.CommandBindings.Add(New CommandBinding(ApplicationCommands.Paste, AddressOf OnPasteRichTextBox))
  48. End Sub
  49. Private Shared Sub AddCommandBindingsToDataGrid(dg As DataGrid)
  50. dg.CommandBindings.Add(New CommandBinding(ApplicationCommands.Copy, AddressOf OnCopyDataGrid))
  51. End Sub
  52. Private Shared Sub SetClipboardText(text As String)
  53. Forms.Clipboard.SetDataObject(text, False, retryTimes:=10, retryDelay:=100) '实际的复制
  54. Try
  55. System.Windows.Clipboard.Flush() '将数据持久化到系统剪贴板,避免应用程序退出后数据丢失
  56. Catch ex As Exception '持久化失败不抛出异常
  57. Logger.Warn(ex, $"剪贴板持久化失败(错误码:0x{ex.HResult:X8})")
  58. End Try
  59. End Sub
  60. Private Shared Sub OnCopyTextBox(sender As Object, e As ExecutedRoutedEventArgs)
  61. Dim tb = TryCast(sender, TextBox)
  62. If tb Is Nothing OrElse tb.SelectionLength <= 0 Then Return
  63. Try
  64. SetClipboardText(tb.SelectedText)
  65. Catch ex As Exception
  66. Logger.Warn(ex, $"复制失败,请稍后再试(错误码:0x{ex.HResult:X8})", LogBehavior.Toast)
  67. End Try
  68. e.Handled = True
  69. End Sub
  70. Private Shared Sub CanEditTextBox(sender As Object, e As CanExecuteRoutedEventArgs)
  71. Dim tb = TryCast(sender, TextBox)
  72. e.CanExecute = tb IsNot Nothing AndAlso Not tb.IsReadOnly AndAlso
  73. (e.Command IsNot ApplicationCommands.Cut OrElse tb.SelectionLength > 0)
  74. e.Handled = True
  75. End Sub
  76. Private Shared Sub OnCutTextBox(sender As Object, e As ExecutedRoutedEventArgs)
  77. Dim tb = TryCast(sender, TextBox)
  78. e.Handled = True
  79. If tb Is Nothing OrElse tb.IsReadOnly OrElse tb.SelectionLength <= 0 Then Return
  80. Try
  81. SetClipboardText(tb.SelectedText)
  82. Catch ex As Exception
  83. Logger.Warn(ex, $"剪切失败,请稍后再试(错误码:0x{ex.HResult:X8})", LogBehavior.Toast)
  84. Return
  85. End Try
  86. tb.SelectedText = String.Empty
  87. End Sub
  88. Private Shared Sub OnPasteTextBox(sender As Object, e As ExecutedRoutedEventArgs)
  89. Dim tb = TryCast(sender, TextBox)
  90. e.Handled = True
  91. If tb Is Nothing OrElse tb.IsReadOnly Then Return
  92. Try
  93. Dim pasteText = Retrier.Attempt(func:=
  94. Function(Attempt) As String
  95. Return If(Forms.Clipboard.ContainsText(), Forms.Clipboard.GetText(), Nothing)
  96. End Function, maxAttempts:=10, delay:=Function() TimeSpan.FromMilliseconds(100))
  97. If pasteText IsNot Nothing Then
  98. '将换行符替换为空格(#7825)
  99. If Not tb.AcceptsReturn Then pasteText = pasteText.ReplaceLineEndings(" ", mergeMultiple:=True)
  100. Dim start = tb.SelectionStart
  101. tb.SelectedText = pasteText
  102. tb.CaretIndex = start + pasteText.Length
  103. tb.SelectionLength = 0
  104. End If
  105. Catch ex As Exception
  106. Logger.Warn(ex, $"粘贴失败,请稍后再试(错误码:0x{ex.HResult:X8})", LogBehavior.Toast)
  107. End Try
  108. End Sub
  109. Private Shared Sub OnCopyRichTextBox(sender As Object, e As ExecutedRoutedEventArgs)
  110. Dim rtb = TryCast(sender, RichTextBox)
  111. If rtb Is Nothing Then Return
  112. Dim textRange = New TextRange(rtb.Selection.Start, rtb.Selection.End)
  113. If String.IsNullOrEmpty(textRange.Text) Then Return
  114. Try
  115. SetClipboardText(textRange.Text)
  116. Catch ex As Exception
  117. Logger.Warn(ex, $"复制失败,请稍后再试(错误码:0x{ex.HResult:X8})", LogBehavior.Toast)
  118. End Try
  119. e.Handled = True
  120. End Sub
  121. Private Shared Sub OnCutRichTextBox(sender As Object, e As ExecutedRoutedEventArgs)
  122. Dim rtb = TryCast(sender, RichTextBox)
  123. If rtb Is Nothing Then Return
  124. Dim selection = New TextRange(rtb.Selection.Start, rtb.Selection.End)
  125. If String.IsNullOrEmpty(selection.Text) Then Return
  126. e.Handled = True
  127. Try
  128. SetClipboardText(selection.Text)
  129. Catch ex As Exception
  130. Logger.Warn(ex, $"剪切失败,请稍后再试(错误码:0x{ex.HResult:X8})", LogBehavior.Toast)
  131. Return
  132. End Try
  133. selection.Text = String.Empty
  134. End Sub
  135. Private Shared Sub OnPasteRichTextBox(sender As Object, e As ExecutedRoutedEventArgs)
  136. Dim rtb = TryCast(sender, RichTextBox)
  137. If rtb Is Nothing Then Return
  138. Try
  139. Dim pasteText = Retrier.Attempt(func:=
  140. Function(Attempt) As String
  141. Return If(Forms.Clipboard.ContainsText(), Forms.Clipboard.GetText(), Nothing)
  142. End Function, maxAttempts:=2, delay:=Function() TimeSpan.FromMilliseconds(200))
  143. If pasteText Is Nothing Then Return
  144. Dim selection = rtb.Selection
  145. selection.Text = pasteText
  146. Dim caretPos = selection.End
  147. rtb.CaretPosition = caretPos
  148. rtb.Selection.Select(caretPos, caretPos)
  149. Catch ex As Exception
  150. Logger.Warn(ex, $"粘贴失败,请稍后再试(错误码:0x{ex.HResult:X8})", LogBehavior.Toast)
  151. End Try
  152. e.Handled = True
  153. End Sub
  154. Private Shared Sub OnCopyDataGrid(sender As Object, e As ExecutedRoutedEventArgs)
  155. Dim dg = TryCast(sender, DataGrid)
  156. If dg Is Nothing OrElse dg.SelectedCells Is Nothing OrElse dg.SelectedCells.Count = 0 Then Return
  157. Dim sb = New StringBuilder
  158. Dim rowGroups = dg.SelectedCells.GroupBy(Function(c) c.Item)
  159. For Each row In rowGroups
  160. Dim rowText = String.Join(vbTab, row.Select(
  161. Function(cell)
  162. Dim tb = TryCast(cell.Column.GetCellContent(cell.Item), TextBlock)
  163. Return If(tb IsNot Nothing, tb.Text, "")
  164. End Function))
  165. sb.AppendLine(rowText)
  166. Next
  167. Dim sbStr = sb.ToString().TrimEnd(ControlChars.Cr, ControlChars.Lf)
  168. Try
  169. SetClipboardText(sbStr)
  170. Catch ex As Exception
  171. Logger.Warn(ex, $"复制失败,请稍后再试(错误码:0x{ex.HResult:X8})", LogBehavior.Toast)
  172. End Try
  173. e.Handled = True
  174. End Sub
  175. End Class
  176. End Namespace