Некоторые гиперссылки не вызывают событие LinkClicked при загрузке файла в RichTextBox - PullRequest
2 голосов
/ 08 июля 2019

В простом приложении Windows Form в .net 4.7 в моей форме есть только RichTextBox. Я загружаю файл * .rtf из моего локального файла, созданный в MS Word 2016. Гиперссылки были установлены в Word. Проблема заключается в том, что не все ссылки вызывают событие LinkClicked при нажатии гиперссылки в приложении.

Поведение выглядит следующим образом: Если гиперссылка сопровождается достаточным количеством символов (которые различаются), она будет вызвана событием LinkClicked Если я удалю символы, которые следуют за гиперссылкой, это событие не вызовет.

Обновление После некоторого тестирования количество символов, которое нужно вставить после последнего URL-адреса, эквивалентно общему количеству символов всех URL-адресов в загружаемом файле * .rtf.

Я не могу опубликовать изображение, слова в скобках - это гиперссылка

Не работает: [Нажмите здесь] для получения дополнительной информации.

{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang4105{\fonttbl{\f0\fnil\fcharset0 Calibri;}}
{\*\generator Riched20 10.0.17134}\viewkind4\uc1 
{\field{\*\fldinst { HYPERLINK "http://www.google.com" }}{\fldrslt {Click here}}}
\pard\sa200\sl276\slmult1\f0\fs22\lang9  for more information.\par
}

Работает: [Для получения дополнительной информации нажмите здесь. Lorem ipsum

{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang4105{\fonttbl{\f0\fnil\fcharset0 Calibri;}}
{\*\generator Riched20 10.0.17134}\viewkind4\uc1 
{\field{\*\fldinst { HYPERLINK "http://www.google.com" }}{\fldrslt {Click here}}}
\pard\sa200\sl276\slmult1\f0\fs22\lang9  for more information. Lorem ipsum\par
}

Количество символов, необходимое для работы ссылки, варьируется от приблизительно 20 до приблизительно 100 символов.

Я создал небольшой проект, чтобы убедиться, что проблема не возникла нигде в основном проекте. Проект содержит только RichTextBox. Я установил для DetectUrls значение True, что не имело значения. Я также попытался создать файл * .rtf в Документах Google, чтобы проверить, может ли быть проблема с версией Word. Я также тестировал с WordPad, включая URL-адреса вручную в Notepad ++. Эта проблема не возникает в .Net Framework 4.6, но у меня есть требование использовать .Net 4.7. Если я добавляю ссылку динамически, проблема также не возникает, но я не могу сделать это в соответствии с моим требованием.

Public Sub Form1_Load(ByVal eventSender As System.Object, ByVal eventArgs As System.EventArgs) Handles MyBase.Load

        Dim LoadFileName As Object

        LoadFileName = "C:\Users\anononym\source\repos\WindowsApp1\Test.rtf"

        RichTextBox1.LoadFile(LoadFileName, RichTextBoxStreamType.RichText)

End Sub

Private Sub RichTextBox_LinkClicked(sender As Object, e As LinkClickedEventArgs) Handles RichTextBox1.LinkClicked
        System.Diagnostics.Process.Start(e.LinkText)
End Sub

Ожидаемый результат - гиперссылка для перенаправления на веб-сайт, установленный в Word, во всех случаях я использовал www.google.com для тестирования.

1 Ответ

1 голос
/ 08 июля 2019

Начиная с .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, содержащим гиперссылку с понятным именем

...