Error al ejecutar TextBox.GetLineText en .NET WPF

Para realizar investigaciones sobre el funcionamiento de programas y sistemas operativos, existen muchas herramientas diferentes. Máquinas virtuales, IDE, blocs de notas inteligentes, IDA, radares, editores hexadecimales, editores pe e incluso más de un centenar de utilidades Sysinternals: todo esto se hace para facilitar muchas operaciones de rutina. Pero a veces llega un momento en que te das cuenta de que entre toda esta diversidad te estás perdiendo una pequeña utilidad que simplemente hará un trabajo banal y simple. Puedes escribir guiones en Python o Powershell en tu rodilla, pero a menudo no puedes mirar esas manualidades sin lágrimas y compartirlas con tus colegas.

Recientemente, esta situación ha vuelto a mí. Y decidí que era hora de tomar y escribir una utilidad ordenada. Hablaré sobre la utilidad en uno de los próximos artículos, pero hablaré sobre uno de los problemas durante el desarrollo ahora.

El error se manifiesta de la siguiente manera: si inserta muchas líneas de texto en el control TextBox estándar, las llamadas a la función GetLineText () a partir de un cierto índice devolverán líneas incorrectas.


Lo incorrecto es que, aunque las líneas serán del texto instalado, pero ubicadas más lejos, de hecho, GetLineText () simplemente omitirá algunas líneas. El error aparece con una gran cantidad de líneas. Entonces la conocí: intenté mostrar 25 megabytes de texto en un TextBox. Trabajar con las últimas líneas reveló un efecto inesperado.

Google sugiere que el error existe desde 2011 y Microsoft no tiene prisa por arreglar algo.

Ejemplo


No hay requisitos particulares para la versión .NET. Creamos un proyecto WPF estándar y completamos los archivos de esta manera:
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 (omitiendo usando y espacio de nombres)

 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(); } } 

La aplicación consta de un cuadro de texto y dos botones. Primero haga clic en "¡Fuego 1!" (llene el cuadro de texto con números), luego "Fire 2!" (solicitará líneas por números e impresión).

Resultado esperado:
req: 150, obtén: 150
req: 300, obtener: 300
req: 450, obtener: 450
req: 600, obtener: 600
req: 750, obtener: 750
req: 900, obtener: 900
req: 15000, obtener: 15000
req: 30000, obtener: 30000
req: 45000, obtenga: 45000
req: 60000, obtener: 60000
req: 75000, obtener: 75000
req: 90000, obtener: 90000

Realidad:



Se puede ver que para índices inferiores a 1000, todo está bien, pero para grandes 15000, los cambios han comenzado. Y cuanto más, más.

Explora el error


Descubrimos la parte del resolutor que es responsable de ver el código fuente .NET y la clase especial "Expansor de oportunidades y vencedor de restricciones basadas en Reflection".

Extensor y limitador de capacidad de reflexión
 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); } } 


Empíricamente, establecemos que en un ejemplo específico, el problema comienza en la región de la línea 8510. Si solicita txt.GetLineText (8510) , se devuelve "8510". Para 8511 - 8511, y para 8512 - de repente, 8513.

Observamos la implementación de GetLineText () en un cuadro de texto:


Omitimos los controles en las primeras líneas y vemos la llamada GetStartPositionOfLine () . Parece que el problema debería estar en esta función, ya que para la línea incorrecta debería volver la posición incorrecta del comienzo de la línea.

Llamamos a nuestro código:

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

Y la verdad: el desplazamiento del primer objeto (el comienzo de la línea 8510) se indica como 49950 caracteres, para el segundo objeto - 49956 y el tercero - 49968. Entre los dos primeros 6 caracteres y entre los siguientes 12. Desorden - esta es la línea que falta.

Entra en GetStartPositionOfLine () :



Nuevamente omitimos las verificaciones de inicio y observamos las acciones reales. Primero, se calcula el punto, que debe ir a la línea con el número lineIndex . Se toma la altura de todas las líneas y se agrega la mitad de la altura de la línea para llegar a su centro. No miramos esto.VerticalOffset y this.HorizontalOffset : son ceros.

Consideramos en nuestro 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; 

Los valores son razonables, correlacionados con la lógica, todo está en orden. Vamos más allá del código GetStartPositionOfLine () : nos interesa la siguiente línea significativa (la primera dentro de la condición), que se parece a un cocodrilo y termina con una llamada a GetTextPositionFromPoint () .

Abrimos los desafíos y los llevamos a través de la reflexión. Tenga en cuenta que algunas interfaces no están disponibles para nosotros debido a restricciones de visibilidad, por lo que debemos referirnos a ellas utilizando la misma reflexión.

 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); 

Los objetos resultantes muestran los mismos desplazamientos: 49950, 49956, 49568. Profundizamos en la implementación de GetTextPositionFromPoint () dentro de TextBoxView.


En, GetLineIndexFromPoint () parece prometedor. Llame a su 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); 

Tenemos 8510, 8511 y 8513 - ¡bingo! A la implementación:



Incluso a simple vista, está claro que esta es una búsqueda binaria. _lineMetrics : una lista de características de cadena (inicio, longitud, ancho del borde). Me froto los bolígrafos con alegría: pensé que, como sucede a menudo, olvidaron pegar +1 en algún lugar o poner > en lugar de > = . Copie la función en el código y depúrelo. Debido a la naturaleza cerrada de los tipos _lineMetrics , lo sacamos a través de reflexiones, _lineHeight lo obtuvimos antes. 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; } 

No llegamos a depurar. o30, o31 y o32 son 8510, 8511 y 8512, respectivamente. ¡Tal como deberían ser! Pero o20, o21 y o22 no están de acuerdo con ellos. ¿Cómo es eso? Casi no cambiamos el código. Casi? Y aquí viene la idea.

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



Esa es la razón: la diferencia es 0.0009375 . Además, si estimamos la acumulación de errores, multiplicamos por 8511, obtenemos 7.9790625. Esto es aproximadamente la mitad de la altura de la línea y, por lo tanto, al calcular las coordenadas, el punto vuela fuera de la línea deseada y cae en la siguiente. La misma variable (en significado) se calculó de dos maneras diferentes y, de repente, no coincidió.

En esto decidí parar. Es posible llegar realmente al fondo de por qué la altura de la columna resultó ser diferente, pero no veo mucho sentido. Es dudoso que Microsoft solucione esto, por lo que buscamos muletas para evitarlo. Muleta de reflexión: establezca el _lineHeigh correcto en uno u otro lugar. Suena tonto, probablemente lento y probablemente poco confiable. O puede mantener su propio conjunto de líneas, paralelas al cuadro de texto, y tomar líneas de él, ya que obtener el número de línea en la posición del cursor funciona correctamente.

Conclusión


Los programadores novatos a menudo pueden escuchar algo sobre errores en el compilador o componentes estándar. En realidad, no son tan comunes, pero aún así nadie está a salvo de ellos. No tenga miedo de mirar dentro de la herramienta que necesita, es emocionante e interesante.

Escribe un buen código!

Otros artículos de blog


Aprendizaje automático en seguridad ofensiva
Ningún auto puede reemplazarme. Muhaha ja. Eso espero.

Dónde insertar comillas en IPv6
Los chicos saben dónde y qué empujar para que sea bueno. Después de esas palabras, seguramente me reemplazarán con un robot. Mié! UHF!

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


All Articles