sources/Google.Solutions.Mvvm/Controls/RichTextBox50.cs (140 lines of code) (raw):

// // Copyright 2023 Google LLC // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // using Google.Solutions.Common.Interop; using System; using System.Diagnostics; using System.Runtime.InteropServices; using System.Windows.Forms; namespace Google.Solutions.Mvvm.Controls { /// <summary> /// Rich text box that uses RICHEDIT50W. /// </summary> public class RichTextBox50 : RichTextBox { //--------------------------------------------------------------------- // Support for friendly links // // The base class has a bug that causes links to not // be clickable under certain conditions, see // https://stackoverflow.com/a/56938772/4372 for details. //--------------------------------------------------------------------- private static unsafe NativeMethods.ENLINK ConvertFromENLINK64(NativeMethods.ENLINK64 es64) { var es = new NativeMethods.ENLINK(); fixed (byte* es64p = &es64.contents[0]) { es.nmhdr = new NativeMethods.NMHDR(); es.charrange = new NativeMethods.CHARRANGE(); es.nmhdr.hwndFrom = Marshal.ReadIntPtr((IntPtr)es64p); es.nmhdr.idFrom = Marshal.ReadIntPtr((IntPtr)(es64p + 8)); es.nmhdr.code = Marshal.ReadInt32((IntPtr)(es64p + 16)); es.msg = Marshal.ReadInt32((IntPtr)(es64p + 24)); es.wParam = Marshal.ReadIntPtr((IntPtr)(es64p + 28)); es.lParam = Marshal.ReadIntPtr((IntPtr)(es64p + 36)); es.charrange.cpMin = Marshal.ReadInt32((IntPtr)(es64p + 44)); es.charrange.cpMax = Marshal.ReadInt32((IntPtr)(es64p + 48)); } return es; } private string CharRangeToString(NativeMethods.CHARRANGE c) { Debug.Assert(c.cpMax > c.cpMin); var txrg = new NativeMethods.TEXTRANGE() { chrg = c }; // // NB. 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 // possibility of Text.Length < c.cpMax // var numCharacters = c.cpMax - c.cpMin + 1; // +1 for null termination if (numCharacters > 0) { using (var buffer = LocalAllocSafeHandle.LocalAlloc((uint)numCharacters * 2)) { txrg.lpstrText = buffer.DangerousGetHandle(); var len = NativeMethods.SendMessage( this.Handle, NativeMethods.EM_GETTEXTRANGE, IntPtr.Zero, txrg); if (len != IntPtr.Zero) { var s = Marshal.PtrToStringUni(buffer.DangerousGetHandle()); Debug.Assert(!string.IsNullOrEmpty(s)); return s; } } } return string.Empty; } protected override void WndProc(ref Message m) { if (m.Msg == NativeMethods.WM_REFLECT + NativeMethods.WM_NOTIFY) { var hdr = (NativeMethods.NMHDR)m.GetLParam(typeof(NativeMethods.NMHDR)); if (hdr.code == NativeMethods.EN_LINK) { NativeMethods.ENLINK lnk; if (IntPtr.Size == 4) { lnk = (NativeMethods.ENLINK)m.GetLParam(typeof(NativeMethods.ENLINK)); } else { lnk = ConvertFromENLINK64( (NativeMethods.ENLINK64)m.GetLParam(typeof(NativeMethods.ENLINK64))); } if (lnk.msg == NativeMethods.WM_LBUTTONDOWN && lnk.charrange != null) { var href = CharRangeToString(lnk.charrange); if (!string.IsNullOrEmpty(href)) { OnLinkClicked(new LinkClickedEventArgs(href)); } m.Result = new IntPtr(1); return; } } } base.WndProc(ref m); } private static class NativeMethods { 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_LBUTTONDOWN = 0x201; internal const int EM_GETTEXTRANGE = WM_USER + 75; public struct NMHDR { public IntPtr hwndFrom; public IntPtr idFrom; public int code; } [StructLayout(LayoutKind.Sequential)] public 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 class ENLINK64 { [MarshalAs(UnmanagedType.ByValArray, SizeConst = 56)] public byte[] contents = new byte[56]; } [StructLayout(LayoutKind.Sequential)] public class CHARRANGE { public int cpMin; public int cpMax; } [StructLayout(LayoutKind.Sequential)] public class TEXTRANGE { public CHARRANGE? chrg; // NB. Allocated by caller, zero terminated by RichEdit public IntPtr lpstrText; } [DllImport("user32.dll")] public static extern IntPtr SendMessage( IntPtr hWnd, int msg, IntPtr wparam, TEXTRANGE lparam); [DllImport( "kernel32.dll", EntryPoint = "LoadLibraryW", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern IntPtr LoadLibraryW(string file); } } }