Como rolar um controle RichTextBox para um determinado ponto, independentemente da posição do cursor

Dec 10 2020

Em meu aplicativo WinForms, tenho um controle RichTextBox que contém um texto longo. Preciso rolar programaticamente para um determinado ponto (expresso como um índice de caractere), independentemente de onde o cursor de seleção está localizado. Preciso de um método como este:

//scroll the control so that the 3512th character is visible.
rtf.ScrollToPosition(3512);

Todas as respostas a perguntas semelhantes que encontrei usam o ScrollToCaret()método, o que é bom se você deseja rolar para a posição do cursor. Mas preciso rolar para uma posição diferente em vez do cursor e sem alterar a posição do cursor. Como eu faço isso?

Obrigado.

Respostas

1 Jimi Dec 11 2020 at 04:09

Você pode usar SendMessage para enviar uma WM_VSCROLLmensagem para o controle RichEdit, especificando SB_THUMBPOSITIONem LOWORDde wParame a posição vertical absoluta para rolar em HIWORD.

O método GetPositionFromCharIndex (pertence a TextBoxBase , por isso também se aplica à classe TextBox) retorna a posição física relativa onde um caractere em uma posição específica é exibido (o valor pode ser negativo se a posição do caractere estiver acima da posição de rolagem atual e for a diferença entre a posição de rolagem atual e a posição do caractere se estiver abaixo dela - a menos que a posição de rolagem atual seja 0).


Suponha que seu RichTextBox seja denominado richTextBox1:

  • Use, por exemplo, Regex.Match para determinar a posição de uma palavra ou frase; a posição do padrão correspondido é retornada pela propriedade Index do Match.
  • Verifique o deslocamento atual com GetPositionFromCharIndex(0)
  • Adicione o valor absoluto do deslocamento definido pela posição vertical atual, ao valor - expresso em pixels - retornado por GetPositionFromCharIndex(matchPos), onde matchPosé a posição de um caractere / palavra / padrão para rolar.
  • Chame SendMessageusando a posição calculada e especificando mover o polegar para esta posição passando SB_THUMBPOSITIONcomo parte de wParam.
var matchPos = Regex.Match(richTextBox1.Text, @"some words").Index;

var pos0 = richTextBox1.GetPositionFromCharIndex(0).Y;
var pos = richTextBox1.GetPositionFromCharIndex(matchPos).Y + Math.Abs(pos0 - 1);

SendMessage(richTextBox1.Handle, WM_VSCROLL, pos << 16 | SB_THUMBPOSITION, 0);

Declaração de métodos nativos:

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern int SendMessage(IntPtr hWnd, uint uMsg, int wParam, int lParam);

private const uint WM_VSCROLL = 0x0115;
private const int SB_THUMBPOSITION = 4;
2 TnTinMn Dec 10 2020 at 10:47

Você pode usar a mesma metodologia implementada no método TextBoxBase.ScrollToCaret para fazer isso. Essa metodologia é baseada no uso do modelo de objeto de texto baseado em COM implementado pelo controle RichEdit subjacente.

O seguinte define o método de extensão ScrollToCharPosition. Ele usa definições abreviadas das interfaces ITextDocument e ITextRange .

public static class RTBExtensions
{

    public static void ScrollToCharPosition(this RichTextBox rtb, Int32 charPosition)
    {
        const Int32 WM_USER = 0x400;
        const Int32 EM_GETOLEINTERFACE = WM_USER + 60;
        const Int32 tomStart = 32;

        if (charPosition < 0 || charPosition > rtb.TextLength - 1)
        {
            throw new ArgumentOutOfRangeException(nameof(charPosition), $"{nameof(charPosition)} must be in the range of 0 to {rtb.TextLength - 1}.");
        }

        // retrieve the rtb's OLEINTERFACE and use the Interop Marshaller to cast it as an ITextDocument
        // The control calls the AddRef method for the object before returning, so the calling application must call the Release method when it is done with the object.
        ITextDocument doc = null;
        SendMessage(new HandleRef(rtb, rtb.Handle), EM_GETOLEINTERFACE, IntPtr.Zero, ref doc);
        ITextRange rng = null;
        if (doc != null)
        {
            try
            {
                rng = (RTBExtensions.ITextRange)doc.Range(charPosition, charPosition);
                rng.ScrollIntoView(tomStart);
            }
            finally
            {
                if (rng != null)
                {
                    Marshal.ReleaseComObject(rng);
                }
                Marshal.ReleaseComObject(doc);
            }
        }
    }

    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    private extern static IntPtr SendMessage(HandleRef hWnd, Int32 msg, IntPtr wParam, ref ITextDocument lParam);

    [ComImport, Guid("8CC497C0-A1DF-11CE-8098-00AA0047BE5D")]
    private interface ITextDocument
    {
        [MethodImpl((short)0, MethodCodeType = MethodCodeType.Runtime)]
        void _VtblGap1_17();
        [return: MarshalAs(UnmanagedType.Interface)]
        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(15)]
        ITextRange Range([In] int cp1, [In] int cp2);
    }

    [ComImport, Guid("8CC497C2-A1DF-11CE-8098-00AA0047BE5D")]
    private interface ITextRange
    {
        [MethodImpl((short)0, MethodCodeType = MethodCodeType.Runtime)]
        void _VtblGap1_49();
        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0x242)]
        void ScrollIntoView([In] int Value);
    }
}

Exemplo de uso :richtextbox1.ScrollToCharPosition(50)