为了对程序和OS的操作进行研究,有很多不同的工具。 虚拟机,IDE,智能笔记本,IDA,雷达,十六进制编辑器,PE编辑器,甚至一百多个Sysinternals实用程序-这样做都是为了促进许多常规操作。 但是有时您会意识到,在所有这些多样性中,您缺少一个小的实用工具,而该实用工具只是简单而平凡的工作。 您可以在膝盖上用python或Powershell编写脚本,但通常情况下,您不能不哭就看这些手工艺品,并与同事分享。
最近,这种情况又来了。 而且我认为是时候开始编写一个简洁的实用程序了。 我将在下一篇文章中介绍该实用程序,但是现在我将介绍开发过程中的问题之一。
该错误表现如下:如果在标准TextBox控件中
插入许多行文本,则从某个索引开始对
GetLineText()函数的调用将返回错误的行。
错误的是,即使这些行将来自已安装的文本,但位于更远的地方,实际上
GetLineText()只会跳过一些行。 出现错误的行数非常多。 所以我遇到了她-我试图在TextBox中显示25 MB的文本。 使用最后几行显示出意想不到的效果。
谷歌
建议该错误自2011年以来就存在 ,微软并不急于修复某些问题。
例子
.NET版本没有特殊要求。 我们创建一个标准的WPF项目,并填写以下文件:
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(使用和命名空间跳过)
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(); } }
该应用程序由一个TextBox和两个按钮组成。 首先点击“ Fire 1!” (用数字填充文本框),然后“触发2!” (它将按数字要求行并打印)。
预期结果:
要求:150,获得:150
req:300,得到:300
要求:450,获取:450
要求:600,获取:600
要求:750,获取:750
要求:900,获取:900
req:15000,得到:15000
要求:30000,获取:30000
req:45000,得到:45000
要求:60000,获取:60000
要求:75000,获取:75000
要求:90000,得到:90000
现实情况:

可以看出,对于小于1000的索引-一切都很好,但是对于15000的较大索引-移位已经开始。 而且,更多。
探索错误
我们揭露了解析器的一部分,该部分负责查看.NET源代码和特殊类“机会扩展器和基于反射的限制克服器”。
反射能力扩展器和限制器 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); } }
根据经验,我们确定在特定示例中,问题开始于线8510的区域。 如果您请求
txt.GetLineText(8510) ,则返回“ 8510”。 对于8511-8511,对于8512-突然是8513。
我们来看一下TextBox上
GetLineText()的实现:

我们跳过第一行中的检查,并看到调用
GetStartPositionOfLine() 。 看来问题应该出在此函数上,因为对于错误的行,应该返回行开头的错误位置。
我们调用我们的代码:
var o00 = txt.InvokeMethod("GetStartPositionOfLine", 8510); var o01 = txt.InvokeMethod("GetStartPositionOfLine", 8511); var o02 = txt.InvokeMethod("GetStartPositionOfLine", 8512);
事实是-第一个对象的偏移量(第8510行的开始)用49950个字符表示,第二个对象-49956和第三个对象-49968表示。在前两个6个字符之间,以及在下一个12个字符之间。乱序-这是缺少的行。
进入
GetStartPositionOfLine() :

再次,我们跳过启动检查,并查看实际操作。 首先,计算该点,该点应转到具有
lineIndex号的行。 取所有线条的高度,然后加上线条高度的一半,以到达其中心。
我们不看
this.VerticalOffset和
this.HorizontalOffset-它们是零。
我们在代码中考虑:
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;
这些值是合理的,与逻辑相关,一切都是有序的。 我们沿着
GetStartPositionOfLine()代码
走得更远 -我们对以下有意义的行(条件中的第一行
)感兴趣,该行看起来像鳄鱼,并以对
GetTextPositionFromPoint()的调用
结尾 。
我们应对挑战,并进行反思。 请注意,由于可见性限制,某些接口对我们不可用,因此我们必须使用相同的反射来引用它们。
var renderScope = (txt.GetFieldValue<FrameworkElement>("_renderScope") as IServiceProvider);
结果对象显示所有相同的偏移量-49950、49956、49568。我们将更深入地了解
TextBoxView内部的GetTextPositionFromPoint
()实现。

在其中,
GetLineIndexFromPoint()看起来很有希望。 调用您的代码。
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);
我们得到8510、8511和8513-宾果游戏! 实施:

即使肉眼可见,这显然是一个二进制搜索。
_lineMetrics-字符串特征(开始,长度,边框宽度)的列表。 我很高兴地擦我的笔-我以为经常发生这种情况,他们忘了将
+1粘在某个地方或放在
>而不是
> =上 。 将函数复制到代码并进行调试。 由于
_lineMetrics类型的封闭性质,
我们通过反射将其拉出,
_lineHeight我们较早就得到了。 总计:
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; }
我们不去调试。 o30,o31和o32分别为8510、8511和8512。 如他们应该! 但是o20,o21和o22不同意。 怎么会这样 我们几乎没有更改代码。 差不多了 洞察力就在这里。
var lh = textView.GetFieldValue<double>("_lineHeight");

这就是原因-差异为
0.0009375 。 此外,如果我们估计错误的累积-我们乘以8511,则得到7.9790625。 这大约是lineHeight的一半,因此,在计算坐标时,该点会飞到所需线的外面并落在下一条线上。 相同的变量(含义)是以两种不同的方式计算的,突然之间不匹配。
我决定停下来。 确实有可能弄明白为什么柱高不一样的原因,但我认为没有什么意义。 微软是否会解决此问题值得怀疑,因此我们将着眼于拐杖。 反射拐杖-
在一个或另一个地方设置正确的
_lineHeigh 。 听起来很蠢,可能很慢,很可能不可靠。 或者,您可以维护自己的与TextBox平行的行集,并从中获取行数,因为在光标位置获取行号可以正常工作。
结论
从新手程序员那里,您经常会听到有关编译器或标准组件中的错误的信息。 实际上,它们并不那么常见,但仍然没有人能够免受它们的伤害。 不要害怕查看所需的工具-它既有趣又有趣。
编写好的代码!
其他博客文章
→
进攻性安全中的机器学习没有汽车可以代替我。 穆哈哈哈。 我希望如此。
→
在IPv6中何处插入引号伙计们知道该在哪里以及在什么方面做得好。 说完这些话,他们肯定会用机器人代替我。 星期三! 超高频!