Erro ao executar o TextBox.GetLineText no .NET WPF

Para realizar pesquisas sobre a operação de programas e sistemas operacionais, existem muitas ferramentas diferentes. Máquinas virtuais, IDEs, blocos de notas inteligentes, IDAs, radares, editores hexadecimais, editores pe e até mais de cem utilitários da Sysinternals - tudo isso é feito para facilitar muitas operações de rotina. Mas às vezes chega um momento em que você percebe que entre toda essa diversidade está perdendo um pequeno utilitário que simplesmente fará um trabalho banal e simples. Você pode escrever scripts em python ou Powershell de joelhos, mas muitas vezes não consegue ver esses trabalhos sem lágrimas e compartilhá-los com seus colegas.

Recentemente, essa situação me ocorreu novamente. E decidi que era hora de apenas pegar e escrever um utilitário interessante. Vou falar sobre o utilitário em um dos próximos artigos, mas vou falar sobre um dos problemas durante o desenvolvimento agora.

O erro se manifesta da seguinte maneira: se você inserir muitas linhas de texto no controle TextBox padrão, as chamadas para a função GetLineText () iniciando em um determinado índice retornarão linhas incorretas.


O errado é que, embora as linhas sejam do texto instalado, mas localizadas mais longe, na verdade, GetLineText () simplesmente ignorará algumas linhas. O erro aparece com um número muito grande de linhas. Então eu a conheci - tentei exibir 25 megabytes de texto em uma caixa de texto. Trabalhar com as últimas linhas revelou um efeito inesperado.

O Google sugere que o erro existe desde 2011 e a Microsoft não tem pressa em corrigir alguma coisa.

Exemplo


Não há requisitos específicos para a versão do .NET. Criamos um projeto WPF padrão e preenchemos os arquivos assim:
MainWindow.xaml

<Window x:Class="wpf_textbox.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:wpf_textbox" mc:Ignorable="d" Title="WTF, WPF?" Height="350" Width="525"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="20"/> <RowDefinition Height="20"/> </Grid.RowDefinitions> <TextBox Grid.Row="0" Margin="5" Name="txt" AcceptsReturn="True" AcceptsTab="True" /> <Button Grid.Row="1" Content="Fire 1!" Click="btn_OnClick" /> <Button Grid.Row="2" Content="Fire 2!" Click="btn2_OnClick" /> </Grid> </Window> 

MainWindow.cs (ignorando usando e espaço para nome)

 public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void btn_OnClick(object sender, RoutedEventArgs e) { var sb = new StringBuilder(); for (int i = 0; i < 90009; i++) sb.AppendLine($"{i}"); txt.Text = sb.ToString(); } private void btn2_OnClick(object sender, RoutedEventArgs e) { var sb = new StringBuilder(); for (var i = 1; i < 7; i++) sb.AppendLine("req: " + 150 * i + ", get: " + txt.GetLineText(150 * i).Trim()); for (var i = 1; i < 7; i++) sb.AppendLine("req: " + 15000 * i + ", get: " + txt.GetLineText(15000 * i).Trim()); txt.Text = sb.ToString(); } } 

O aplicativo consiste em um TextBox e dois botões. Primeiro clique em "Fire 1!" (preencha o TextBox com números) e, em seguida, "Disparar 2!" (solicitará linhas por números e será impresso).

Resultado esperado:
req: 150, obtenha: 150
req: 300, get: 300
req: 450, obtenha: 450
req: 600, obtenha: 600
req: 750, get: 750
req: 900, obtenha: 900
req: 15000, obtenha: 15000
req: 30000, obtenha: 30000
req: 45000, obtenha: 45000
req: 60000, obtenha: 60000
req: 75000, obtenha: 75000
req: 90000, obtenha: 90000

Realidade:



Pode-se observar que, para índices inferiores a 1000 - está tudo bem, mas para 15 mil - as mudanças começaram. E quanto mais, mais.

Explore o bug


Descobrimos a parte do resolvedor responsável por visualizar o código-fonte .NET e a classe especial “Expansor de oportunidades e vencedor de restrições baseadas no Reflection”.

Extensor e limitador de capacidade de reflexão
 public static class ReflectionExtensions { public static T GetFieldValue<T>(this object obj, string name) { var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var field = obj.GetType().GetField(name, bindingFlags); if (field == null) field = obj.GetType().BaseType.GetField(name, bindingFlags); return (T)field?.GetValue(obj); } public static object InvokeMethod(this object obj, string methodName, params object[] methodParams) { var methodParamTypes = methodParams?.Select(p => p.GetType()).ToArray() ?? new Type[] { }; var bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static; MethodInfo method = null; var type = obj.GetType(); while (method == null && type != null) { method = type.GetMethod(methodName, bindingFlags, Type.DefaultBinder, methodParamTypes, null); var intfs = type.GetInterfaces(); if (method != null) break; foreach (var intf in intfs) { method = intf.GetMethod(methodName, bindingFlags, Type.DefaultBinder, methodParamTypes, null); if (method != null) break; } type = type.BaseType; } return method?.Invoke(obj, methodParams); } } 


Empiricamente, estabelecemos que, em um exemplo específico, o problema começa na região da linha 8510. Se você solicitar txt.GetLineText (8510) , "8510" retornará. Para 8511 - 8511 e para 8512 - de repente, 8513.

Examinamos a implementação de GetLineText () em um TextBox:


Ignoramos as verificações nas primeiras linhas e vemos a chamada GetStartPositionOfLine () . Parece que o problema deve estar nessa função, pois para a linha errada a posição incorreta do início da linha deve retornar.

Chamamos em nosso código:

 var o00 = txt.InvokeMethod("GetStartPositionOfLine", 8510); var o01 = txt.InvokeMethod("GetStartPositionOfLine", 8511); var o02 = txt.InvokeMethod("GetStartPositionOfLine", 8512); 

E a verdade - o deslocamento do primeiro objeto (o início da 8510ª linha) é indicado como 49950 caracteres, para o segundo objeto - 49956 e o ​​terceiro - 49968. Entre os dois primeiros 6 caracteres e entre os próximos 12. O distúrbio é a linha que falta.

Acesse GetStartPositionOfLine () :



Mais uma vez, pulamos as verificações iniciais e analisamos as ações reais. Primeiro, o ponto é calculado, o que deve ir para a linha com o número lineIndex . A altura de todas as linhas é obtida e metade da altura da linha é adicionada para chegar ao centro. Não olhamos para isso. VerticalOffset e this.HorizontalOffset - são zeros.

Consideramos em nosso código:

 var lineHeight = (double) txt.InvokeMethod("GetLineHeight", null); var y0 = lineHeight * (double)8510 + lineHeight / 2.0 - txt.VerticalOffset; var y1 = lineHeight * (double)8511 + lineHeight / 2.0 - txt.VerticalOffset; var y2 = lineHeight * (double)8512 + lineHeight / 2.0 - txt.VerticalOffset; 

Os valores são razoáveis, correlacionados com a lógica, tudo está em ordem. Vamos mais além no código GetStartPositionOfLine () - estamos interessados ​​na seguinte linha significativa (a primeira dentro da condição), que parece um crocodilo e termina com uma chamada para GetTextPositionFromPoint () .

Abrimos os desafios e os atraímos para a reflexão. Observe que algumas interfaces não estão disponíveis para nós devido a restrições de visibilidade; portanto, precisamos nos referir a elas usando a mesma reflexão.

 var renderScope = (txt.GetFieldValue<FrameworkElement>("_renderScope") as IServiceProvider); // 7 -   ITextView var textView = renderScope.GetService(renderScope.GetType().GetInterfaces()[7]); var o10 = textView.InvokeMethod("GetTextPositionFromPoint", new Point(-txt.HorizontalOffset, y0), true); var o11 = textView.InvokeMethod("GetTextPositionFromPoint", new Point(-txt.HorizontalOffset, y1), true); var o12 = textView.InvokeMethod("GetTextPositionFromPoint", new Point(-txt.HorizontalOffset, y2), true); 

Os objetos resultantes mostram todos os mesmos deslocamentos - 49950, 49956, 49568. Aprofundamos a implementação GetTextPositionFromPoint () dentro do TextBoxView.


Em, GetLineIndexFromPoint () parece promissor. Ligue no seu código.

 var o20 = textView.InvokeMethod("GetLineIndexFromPoint", new Point(-txt.HorizontalOffset, y0), true); var o21 = textView.InvokeMethod("GetLineIndexFromPoint", new Point(-txt.HorizontalOffset, y1), true); var o22 = textView.InvokeMethod("GetLineIndexFromPoint", new Point(-txt.HorizontalOffset, y2), true); 

Temos 8510, 8511 e 8513 - bingo! Para implementação:



Mesmo a olho nu, é claro que esta é uma pesquisa binária. _lineMetrics - uma lista de características de sequência (início, comprimento, largura da borda). Esfrego alegremente minhas canetas - pensei que, como acontece com frequência, elas esqueceram de colar +1 em algum lugar ou colocar > em vez de > = . Copie a função para o código e depure-o. Devido à natureza fechada dos tipos _lineMetrics , nós o extraímos através de reflexões, _lineHeight , nós o obtemos anteriormente. Total:

 var lm = textView.GetFieldValue<object>("_lineMetrics"); var c = (int)lm.InvokeMethod("get_Count"); var lineMetrics = new List<Tuple<int,int,int,double>>(); for (var i = 0; i < c; i++) { var arr_o = lm.InvokeMethod("get_Item", i); var contLength = arr_o.GetFieldValue<int>("_contentLength"); var length = arr_o.GetFieldValue<int>("_length"); var offset = arr_o.GetFieldValue<int>("_offset"); var width = arr_o.GetFieldValue<double>("_width"); lineMetrics.Add(new Tuple<int, int, int, double>(contLength, length, offset, width)); } var o30 = GetLineIndexFromPoint(lineMetrics, lineHeight, new Point(-txt.HorizontalOffset, y0), true); var o31 = GetLineIndexFromPoint(lineMetrics, lineHeight, new Point(-txt.HorizontalOffset, y1), true); var o32 = GetLineIndexFromPoint(lineMetrics, lineHeight, new Point(-txt.HorizontalOffset, y2), true); /*<...>*/ private int GetLineIndexFromPoint(List<Tuple<int, int, int, double>> lm, double _lineHeight, Point point, bool snapToText) { if (point.Y < 0.0) return !snapToText ? -1 : 0; if (point.Y >= _lineHeight * (double)lm.Count) { if (!snapToText) return -1; return lm.Count - 1; } int index = -1; int num1 = 0; int num2 = lm.Count; while (num1 < num2) { index = num1 + (num2 - num1) / 2; var lineMetric = lm[index]; double num3 = _lineHeight * (double)index; if (point.Y < num3) num2 = index; else if (point.Y >= num3 + _lineHeight) { num1 = index + 1; } else { if (!snapToText && (point.X < 0.0 || point.X >= lineMetric.Item4)) { index = -1; break; } break; } } if (num1 >= num2) return -1; return index; } 

Não chegamos à depuração. o30, o31 e o32 são 8510, 8511 e 8512, respectivamente. Como deveriam ser! Mas o20, o21 e o22 não concordam com eles. Como assim? Quase não mudamos o código. Quase? E aqui vem a visão.

 var lh = textView.GetFieldValue<double>("_lineHeight"); 



Essa é a razão - a diferença é 0.0009375 . Além disso, se estimarmos o acúmulo de erros - multiplicamos por 8511, obtemos 7,9790625. Isso é quase metade da linhaHeight e, portanto, ao calcular as coordenadas, o ponto voa para fora da linha desejada e cai na próxima. A mesma variável (em significado) foi calculada de duas maneiras diferentes e, de repente, não correspondeu.

Por isso, decidi parar. É possível realmente entender por que a altura da coluna foi diferente, mas não vejo muito sentido. É duvidoso que a Microsoft conserte isso, por isso procuramos muletas para contornar. Muleta de reflexão - defina a _lineHeigh correta em um ou outro local. Parece idiota, provavelmente lento e provavelmente não confiável. Ou você pode manter seu próprio conjunto de linhas, paralelo ao TextBox, e obter linhas dele, pois a obtenção do número da linha na posição do cursor funciona corretamente.

Conclusão


De programadores iniciantes, você pode ouvir algo sobre bugs no compilador ou nos componentes padrão. Na realidade, eles não são tão comuns, mas ainda assim ninguém está a salvo deles. Não tenha medo de olhar dentro da ferramenta que você precisa - é emocionante e interessante.

Escreva um bom código!

Outros artigos do blog


Aprendizado de máquina com segurança ofensiva
Nenhum carro pode me substituir. Muhaha ha. Espero que sim.

Onde inserir aspas no IPv6
Os caras sabem onde e o que empurrar para melhorar. Após essas palavras, eles vão me substituir por um robô, com certeza. Qua! UHF!

Source: https://habr.com/ru/post/pt420361/


All Articles