winforms 将文件加载到RichTextBox中时,某些超链接不会触发LinkClicked事件

vwoqyblh  于 2022-11-17  发布在  其他
关注(0)|答案(2)|浏览(229)

在.NET 4.7中的一个简单Windows窗体应用程序中,我的窗体上只有一个RichTextBox。我正在从本地加载一个在MS Word 2016中创建的 *.rtf文件。超链接已在Word中设置。问题是,当单击应用程序中的超链接时,并非所有链接都触发LinkClicked事件。

**行为如下:**如果超链接后面有足够多的字符(视情况而定),它将由LinkClicked事件触发。如果删除超链接后面的字符,它将不会触发该事件。

在做了一些测试之后,需要在最后一个URL之后插入的字符数等于正在加载的 *.rtf文件中所有URL的字符总数。

  • 我无法上传图片,括号中的文字是超链接 *
    不工作:[单击此处]了解更多信息。
{\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
}

Works:[点击这里]了解更多信息。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,这没有任何区别。我还尝试在Google Docs中创建 *.rtf文件,以检查是否是Word的版本问题。我还使用写字板进行了测试。在Notepad++中手动添加URL。在.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进行测试。

kq0g1dla

kq0g1dla1#

从.NET 4.7开始,RichTextBox使用RichEdit 50控件;以前的版本使用的是RichEdit 20控件。2我不知道两个控件版本在处理超链接时有什么不同,但很明显是有一些不同的。
一种解决方法是将.NET 4.7应用程序配置为使用较旧的控件。这可以通过将以下内容添加到App.config文件中来完成。

<runtime>
  <AppContextSwitchOverrides value="Switch.System.Windows.Forms.DoNotLoadLatestRichEditControl=true" />
</runtime>

问题的根源似乎是原始RichTextBox.CharRangeToString方法中的一个黑客。

//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;
        }

使用RichEdit 50控件中可用的Friendly Name Hyperlinks时,RichTextBox.Text.Length属性可以小于c.cpMax值,因为链接未包含在传回的属性值中。这会导致方法将String.Empty传回呼叫的RichTextBox.EnLinkMsgHandler方法,而呼叫的RichTextBox.EnLinkMsgHandler方法在传回Empty.String时不会引发LickClicked事件。

case NativeMethods.WM_LBUTTONDOWN:
                string linktext = CharRangeToString(enlink.charrange);
                if (!string.IsNullOrEmpty(linktext))
                {
                    OnLinkClicked(new LinkClickedEventArgs(linktext));
                }
                m.Result = (IntPtr)1;
                return;

为了处理这个 bug,下面定义了一个定制的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控件的地方使用它。
此问题已在MS开发人员社区上发布为:WinForm RichTextBox LinkClicked event fails to fire when control loaded with RTF containing a friendly name hyperlink

rm5edbpk

rm5edbpk2#

这是一个@TnTinMn的VB实现的C#转换,它对我很有效。感谢https://github.com/icsharpcode/CodeConverter完成了大部分的转换。
显然,根据需要更改下面的命名空间。再次感谢,@TnTinMn。
RichTextBoxFixedForFriendlyLinks.cs

using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using static TestRtf.NativeMthods;

namespace TestRtf
{
    public partial class RichTextBoxFixedForFriendlyLinks : RichTextBox
    {
        internal ENLINK ConvertFromENLINK64(ENLINK64 es64)
        {
            // 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

            var es = new ENLINK();
            GCHandle? hndl = null;
            try
            {
                hndl = GCHandle.Alloc(es64.contents, GCHandleType.Pinned);
                var es64p = hndl.Value.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
            {
                if (hndl.HasValue)
                    hndl.Value.Free();
            }

            return es;
        }

        protected override void WndProc(ref Message m)
        {
            if (m.Msg == WM_ReflectNotify)
            {
                NMHDR hdr = (NMHDR)m.GetLParam(typeof(NMHDR));
                if (hdr.code == EN_Link)
                {
                    ENLINK lnk;
                    if (IntPtr.Size == 4)
                    {
                        lnk = (ENLINK)m.GetLParam(typeof(ENLINK));
                    }
                    else
                    {
                        lnk = ConvertFromENLINK64((ENLINK64)m.GetLParam(typeof(ENLINK64)));
                    }

                    if (lnk.msg == WM_LBUTTONDOWN)
                    {
                        string linkUrl = CharRangeToString(lnk.charrange);
                        // Still check if linkUrl is not empty
                        if (!string.IsNullOrEmpty(linkUrl))
                        {
                            OnLinkClicked(new LinkClickedEventArgs(linkUrl));
                        }

                        m.Result = new IntPtr(1);
                        return;
                    }
                }
            }

            base.WndProc(ref m);
        }

        private string CharRangeToString(CHARRANGE c)
        {
            string ret = string.Empty;
            var txrg = new TEXTRANGE() { 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
            // *********

            int numCharacters = c.cpMax - c.cpMin + 1; // +1 for null termination
            if (numCharacters > 0)
            {
                var charBuffer = default(CharBuffer);
                charBuffer = CharBuffer.CreateBuffer(numCharacters);
                IntPtr unmanagedBuffer = IntPtr.Zero;
                try
                {
                    unmanagedBuffer = charBuffer.AllocCoTaskMem();
                    if (unmanagedBuffer == IntPtr.Zero)
                    {
                        throw new OutOfMemoryException();
                    }

                    txrg.lpstrText = unmanagedBuffer;
                    IntPtr len = SendMessage(new HandleRef(this, Handle), EM_GETTEXTRANGE, 0, txrg);
                    if (len != IntPtr.Zero)
                    {
                        charBuffer.PutCoTaskMem(unmanagedBuffer);
                        ret = charBuffer.GetString();
                    }
                }
                finally
                {
                    if (txrg.lpstrText != IntPtr.Zero)
                    {
                        Marshal.FreeCoTaskMem(unmanagedBuffer);
                    }
                }
            }

            return ret;
        }
    }
}

NativeMthods.cs:

using System;
using System.Runtime.InteropServices;
using System.Text;

namespace TestRtf
{
    public partial class NativeMthods
    {
        internal const int EN_Link = 0x70B;
        internal const int WM_NOTIFY = 0x4E;
        internal const int WM_User = 0x400;
        internal const int WM_REFLECT = WM_User + 0x1C00;
        internal const int WM_ReflectNotify = WM_REFLECT | WM_NOTIFY;
        internal const int WM_LBUTTONDOWN = 0x201;
        internal const int EM_GETTEXTRANGE = WM_User + 75;

        public partial struct NMHDR
        {
            public IntPtr hwndFrom;
            public IntPtr idFrom; // This is declared as UINT_PTR in winuser.h
            public int code;
        }

        [StructLayout(LayoutKind.Sequential)]
        public partial class ENLINK
        {
            public NMHDR nmhdr;
            public int msg = 0;
            public IntPtr wParam = IntPtr.Zero;
            public IntPtr lParam = IntPtr.Zero;
            public CHARRANGE charrange = null;
        }

        [StructLayout(LayoutKind.Sequential)]
        public partial class ENLINK64
        {
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 56)]
            public byte[] contents = new byte[56];
        }

        [StructLayout(LayoutKind.Sequential)]
        public partial class CHARRANGE
        {
            public int cpMin;
            public int cpMax;
        }

        [StructLayout(LayoutKind.Sequential)]
        public partial class TEXTRANGE
        {
            public CHARRANGE chrg;
            public IntPtr lpstrText; // allocated by caller, zero terminated by RichEdit
        }

        public abstract partial class CharBuffer
        {
            public static CharBuffer CreateBuffer(int size)
            {
                if (Marshal.SystemDefaultCharSize == 1)
                {
                    return new AnsiCharBuffer(size);
                }

                return new UnicodeCharBuffer(size);
            }

            public abstract IntPtr AllocCoTaskMem();
            public abstract string GetString();
            public abstract void PutCoTaskMem(IntPtr ptr);
            public abstract void PutString(string s);
        }

        public partial class AnsiCharBuffer : CharBuffer
        {
            internal byte[] buffer;
            internal int offset;

            public AnsiCharBuffer(int size)
            {
                buffer = new byte[size];
            }

            public override IntPtr AllocCoTaskMem()
            {
                var result = Marshal.AllocCoTaskMem(buffer.Length);
                Marshal.Copy(buffer, 0, result, buffer.Length);
                return result;
            }

            public override string GetString()
            {
                int i = offset;
                while (i < buffer.Length && buffer[i] != 0)
                    i += 1;
                string result = Encoding.Default.GetString(buffer, offset, i - offset);
                if (i < buffer.Length)
                {
                    i += 1;
                }

                offset = i;
                return result;
            }

            public override void PutCoTaskMem(IntPtr ptr)
            {
                Marshal.Copy(ptr, buffer, 0, buffer.Length);
                offset = 0;
            }

            public override void PutString(string s)
            {
                var bytes = Encoding.Default.GetBytes(s);
                int count = Math.Min(bytes.Length, buffer.Length - offset);
                Array.Copy(bytes, 0, buffer, offset, count);
                offset += count;
                if (offset < buffer.Length)
                {
                    buffer[offset] = 0;
                    offset += 1;
                }
            }
        }

        public partial class UnicodeCharBuffer : CharBuffer
        {
            internal char[] buffer;
            internal int offset;

            public UnicodeCharBuffer(int size)
            {
                buffer = new char[size];
            }

            public override IntPtr AllocCoTaskMem()
            {
                var result = Marshal.AllocCoTaskMem(buffer.Length * 2);
                Marshal.Copy(buffer, 0, result, buffer.Length);
                return result;
            }

            public override string GetString()
            {
                int i = offset;
                while (i < buffer.Length && buffer[i] != 0)
                    i += 1;
                string result = new string(buffer, offset, i - offset);
                if (i < buffer.Length)
                {
                    i += 1;
                }

                offset = i;
                return result;
            }

            public override void PutCoTaskMem(IntPtr ptr)
            {
                Marshal.Copy(ptr, buffer, 0, buffer.Length);
                offset = 0;
            }

            public override void PutString(string s)
            {
                int count = Math.Min(s.Length, buffer.Length - offset);
                s.CopyTo(0, buffer, offset, count);
                offset += count;
                if (offset < buffer.Length)
                {
                    buffer[offset] = '\0';
                    offset += 1;
                }
            }
        }

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessage(HandleRef hWnd, int msg, int wParam, TEXTRANGE lParam);
    }
}

相关问题