Начиная с .Net 4.7, RichTextBox использует элемент управления RichEdit50; В предыдущих версиях использовался элемент управления RichEdit20. Я не знаю причину различий в обработке гиперссылок между контрольными версиями, но, очевидно, есть некоторые различия.
Обходной путь - настроить приложение .Net 4.7 для использования старого элемента управления. Это делается путем добавления следующего к вашему App.config
файлу.
<runtime>
<AppContextSwitchOverrides value="Switch.System.Windows.Forms.DoNotLoadLatestRichEditControl=true" />
</runtime>
Редактировать:
Источником проблемы является взлом в оригинальном RichTextBox.CharRangeToString Method .
//Windows bug: 64-bit windows returns a bad range for us. VSWhidbey 504502.
//Putting in a hack to avoid an unhandled exception.
if (c.cpMax > Text.Length || c.cpMax-c.cpMin <= 0) {
return string.Empty;
}
При использовании гиперссылок Friendly Name , доступных в элементе управления RichEdit50, свойство RichTextBox.Text.Length
может быть меньше значения c.cpMax
, так как ссылка не включена в возвращаемое значение свойства. Это приводит к тому, что метод возвращает String.Empty
в вызывающий RichTextBox.EnLinkMsgHandler метод , который, в свою очередь, не вызовет событие LickClicked, если возвращается Empty.String
.
case NativeMethods.WM_LBUTTONDOWN:
string linktext = CharRangeToString(enlink.charrange);
if (!string.IsNullOrEmpty(linktext))
{
OnLinkClicked(new LinkClickedEventArgs(linktext));
}
m.Result = (IntPtr)1;
return;
Чтобы справиться с этой ошибкой , ниже определен пользовательский класс RichTextBox
, чтобы изменить логику метода CharRangeToString
. Эта измененная логика вызывается в процедуре WndProc
для обхода логики по умолчанию.
Imports System.Runtime.InteropServices
Imports WindowsApp2.NativeMthods ' *** change WindowsApp2 to match your project
Public Class RichTextBoxFixedForFriendlyLinks : Inherits RichTextBox
Friend Function ConvertFromENLINK64(es64 As ENLINK64) As ENLINK
' Note: the RichTextBox.ConvertFromENLINK64 method is written using C# unsafe code
' this is version uses a GCHandle to pin the byte array so that
' the same Marshal.Read_Xyz methods can be used
Dim es As New ENLINK()
Dim hndl As GCHandle
Try
hndl = GCHandle.Alloc(es64.contents, GCHandleType.Pinned)
Dim es64p As IntPtr = hndl.AddrOfPinnedObject
es.nmhdr = New NMHDR()
es.charrange = New CHARRANGE()
es.nmhdr.hwndFrom = Marshal.ReadIntPtr(es64p)
es.nmhdr.idFrom = Marshal.ReadIntPtr(es64p + 8)
es.nmhdr.code = Marshal.ReadInt32(es64p + 16)
es.msg = Marshal.ReadInt32(es64p + 24)
es.wParam = Marshal.ReadIntPtr(es64p + 28)
es.lParam = Marshal.ReadIntPtr(es64p + 36)
es.charrange.cpMin = Marshal.ReadInt32(es64p + 44)
es.charrange.cpMax = Marshal.ReadInt32(es64p + 48)
Finally
hndl.Free()
End Try
Return es
End Function
Protected Overrides Sub WndProc(ByRef m As Message)
If m.Msg = WM_ReflectNotify Then
Dim hdr As NMHDR = CType(m.GetLParam(GetType(NMHDR)), NMHDR)
If hdr.code = EN_Link Then
Dim lnk As ENLINK
If IntPtr.Size = 4 Then
lnk = CType(m.GetLParam(GetType(ENLINK)), ENLINK)
Else
lnk = ConvertFromENLINK64(CType(m.GetLParam(GetType(ENLINK64)), ENLINK64))
End If
If lnk.msg = WM_LBUTTONDOWN Then
Dim linkUrl As String = CharRangeToString(lnk.charrange)
' Still check if linkUrl is not empty
If Not String.IsNullOrEmpty(linkUrl) Then
OnLinkClicked(New LinkClickedEventArgs(linkUrl))
End If
m.Result = New IntPtr(1)
Exit Sub
End If
End If
End If
MyBase.WndProc(m)
End Sub
Private Function CharRangeToString(ByVal c As CHARRANGE) As String
Dim ret As String = String.Empty
Dim txrg As New TEXTRANGE With {.chrg = c}
''Windows bug: 64-bit windows returns a bad range for us. VSWhidbey 504502.
''Putting in a hack to avoid an unhandled exception.
'If c.cpMax > Text.Length OrElse c.cpMax - c.cpMin <= 0 Then
' Return String.Empty
'End If
' *********
' c.cpMax can be greater than Text.Length if using friendly links
' with RichEdit50. so that check is not valid.
' instead of the hack above, first check that the number of characters is positive
' and then use the result of sending EM_GETTEXTRANGE to handle the
' possibilty of Text.Length < c.cpMax
' *********
Dim numCharacters As Int32 = (c.cpMax - c.cpMin) + 1 ' +1 for null termination
If numCharacters > 0 Then
Dim charBuffer As CharBuffer
charBuffer = CharBuffer.CreateBuffer(numCharacters)
Dim unmanagedBuffer As IntPtr
Try
unmanagedBuffer = charBuffer.AllocCoTaskMem()
If unmanagedBuffer = IntPtr.Zero Then
Throw New OutOfMemoryException()
End If
txrg.lpstrText = unmanagedBuffer
Dim len As Int32 = CInt(SendMessage(New HandleRef(Me, Handle), EM_GETTEXTRANGE, 0, txrg))
If len > 0 Then
charBuffer.PutCoTaskMem(unmanagedBuffer)
ret = charBuffer.GetString()
End If
Finally
If txrg.lpstrText <> IntPtr.Zero Then
Marshal.FreeCoTaskMem(unmanagedBuffer)
End If
End Try
End If
Return ret
End Function
End Class
Хотя приведенный выше код не настолько существенен, он требует нескольких методов / структур из базовой реализации, которые не являются общедоступными. Версия методов VB представлена ниже. Большинство из них являются прямыми преобразованиями из исходного источника C #.
Imports System.Runtime.InteropServices
Imports System.Text
Public Class NativeMthods
Friend Const EN_Link As Int32 = &H70B
Friend Const WM_NOTIFY As Int32 = &H4E
Friend Const WM_User As Int32 = &H400
Friend Const WM_REFLECT As Int32 = WM_User + &H1C00
Friend Const WM_ReflectNotify As Int32 = WM_REFLECT Or WM_NOTIFY
Friend Const WM_LBUTTONDOWN As Int32 = &H201
Friend Const EM_GETTEXTRANGE As Int32 = WM_User + 75
Public Structure NMHDR
Public hwndFrom As IntPtr
Public idFrom As IntPtr 'This is declared as UINT_PTR in winuser.h
Public code As Int32
End Structure
<StructLayout(LayoutKind.Sequential)>
Public Class ENLINK
Public nmhdr As NMHDR
Public msg As Int32 = 0
Public wParam As IntPtr = IntPtr.Zero
Public lParam As IntPtr = IntPtr.Zero
Public charrange As CHARRANGE = Nothing
End Class
<StructLayout(LayoutKind.Sequential)>
Public Class ENLINK64
<MarshalAs(UnmanagedType.ByValArray, SizeConst:=56)>
Public contents(0 To 55) As Byte
End Class
<StructLayout(LayoutKind.Sequential)>
Public Class CHARRANGE
Public cpMin As Int32
Public cpMax As Int32
End Class
<StructLayout(LayoutKind.Sequential)>
Public Class TEXTRANGE
Public chrg As CHARRANGE
Public lpstrText As IntPtr ' allocated by caller, zero terminated by RichEdit
End Class
Public MustInherit Class CharBuffer
Public Shared Function CreateBuffer(ByVal size As Int32) As CharBuffer
If Marshal.SystemDefaultCharSize = 1 Then
Return New AnsiCharBuffer(size)
End If
Return New UnicodeCharBuffer(size)
End Function
Public MustOverride Function AllocCoTaskMem() As IntPtr
Public MustOverride Function GetString() As String
Public MustOverride Sub PutCoTaskMem(ByVal ptr As IntPtr)
Public MustOverride Sub PutString(ByVal s As String)
End Class
Public Class AnsiCharBuffer : Inherits CharBuffer
Friend buffer() As Byte
Friend offset As Int32
Public Sub New(ByVal size As Int32)
buffer = New Byte(0 To size - 1) {}
End Sub
Public Overrides Function AllocCoTaskMem() As IntPtr
Dim result As IntPtr = Marshal.AllocCoTaskMem(buffer.Length)
Marshal.Copy(buffer, 0, result, buffer.Length)
Return result
End Function
Public Overrides Function GetString() As String
Dim i As Int32 = offset
Do While i < buffer.Length AndAlso buffer(i) <> 0
i += 1
Loop
Dim result As String = Encoding.Default.GetString(buffer, offset, i - offset)
If i < buffer.Length Then
i += 1
End If
offset = i
Return result
End Function
Public Overrides Sub PutCoTaskMem(ByVal ptr As IntPtr)
Marshal.Copy(ptr, buffer, 0, buffer.Length)
offset = 0
End Sub
Public Overrides Sub PutString(ByVal s As String)
Dim bytes() As Byte = Encoding.Default.GetBytes(s)
Dim count As Int32 = Math.Min(bytes.Length, buffer.Length - offset)
Array.Copy(bytes, 0, buffer, offset, count)
offset += count
If offset < buffer.Length Then
buffer(offset) = 0
offset += 1
End If
End Sub
End Class
Public Class UnicodeCharBuffer : Inherits CharBuffer
Friend buffer() As Char
Friend offset As Int32
Public Sub New(ByVal size As Int32)
buffer = New Char(size - 1) {}
End Sub
Public Overrides Function AllocCoTaskMem() As IntPtr
Dim result As IntPtr = Marshal.AllocCoTaskMem(buffer.Length * 2)
Marshal.Copy(buffer, 0, result, buffer.Length)
Return result
End Function
Public Overrides Function GetString() As String
Dim i As Int32 = offset
Do While i < buffer.Length AndAlso AscW(buffer(i)) <> 0
i += 1
Loop
Dim result As New String(buffer, offset, i - offset)
If i < buffer.Length Then
i += 1
End If
offset = i
Return result
End Function
Public Overrides Sub PutCoTaskMem(ByVal ptr As IntPtr)
Marshal.Copy(ptr, buffer, 0, buffer.Length)
offset = 0
End Sub
Public Overrides Sub PutString(ByVal s As String)
Dim count As Int32 = Math.Min(s.Length, buffer.Length - offset)
s.CopyTo(0, buffer, offset, count)
offset += count
If offset < buffer.Length Then
buffer(offset) = ChrW(0)
offset += 1
End If
End Sub
End Class
<DllImport("user32.dll", CharSet:=CharSet.Auto)>
Public Shared Function SendMessage(ByVal hWnd As HandleRef, ByVal msg As Int32, ByVal wParam As Int32, ByVal lParam As TEXTRANGE) As IntPtr
End Function
End Class
Добавьте эти классы в наш проект и выполните сборку. RichTextBoxFixedForFriendlyLinks
должно быть доступно на панели инструментов. Вы можете использовать его там, где обычно используете элемент управления RichTextBox
.
Редактировать 2:
Эта проблема была опубликована в сообществе разработчиков MS как: Событие WinForm RichTextBox LinkClicked не срабатывает, когда элемент управления загружен RTF, содержащим гиперссылку с понятным именем