在`Winform`中读取晶圆测试档案并绘制晶圆图
本文最后更新于:2025年4月10日 晚上
在 Winform 中读取晶圆测试档案并绘制晶圆图
写在最前面
在摸鱼的时候想到什么就写什么的一个小程序,功能并不完善,以及 Bug 频发(总之先叠甲吧)。主要是为以后设计此类程序提供一个大致思路。(?
仓库地址:DrawWaferMapApp(由于某些原因仓库已经更改为 Private,源码请联系邮箱:Centaurea_G@hotmail.com)
本文比较有用的部分:晶圆图控件 WaferMap
介绍
(但是写完本文后我发现本文起到的作用更多是文档而不是教程,呃,这就有点尴尬了)
CSV 处理工具介绍
Coordinate
用于表示坐标的类,跟 Point
类没啥区别。
Constructor | Description |
---|---|
Coordinate |
初始化一个 Coordinate 类的新实例 |
Coordinate(int, int) |
使用指定的值初始化一个 Coordinate 类的新实例 |
Property | Description |
---|---|
X |
表示平面直角坐标系横坐标 |
Y |
表示平面直角坐标系纵坐标 |
CsvTemplate
帮助解析 csv 文件用,它告诉 CsvProcessTool
该如何解析 csv 文件。
Property | Description |
---|---|
HeaderRowStartNumber |
获取或设置表头行起始行号, 行号从 1 开始计数 |
HeaderRowEndNumber |
获取或设置表头行结束行号, 行号从 1 开始计数 |
HeaderKeyColumnNumber |
获取或设置表头信息列号, 列号从 1 开始计数, 默认为 1 |
HeaderValueColumnNumber |
获取或设置表头信息值起始列号, 列号从 1 开始计数, 默认为 2 |
ColumnNameRowNumber |
获取或设置数据列列名行号, 行号从 1 开始计数 |
DataRowStartNumber |
获取或设置数据行起始行号, 行号从 1 开始计数 |
XCoordinateColumnNumber |
获取或设置横坐标列号, 列号从 1 开始计数 |
YCoordinateColumnNumber |
获取或设置纵坐标列号, 列号从 1 开始计数 |
ColumnNames |
获取或设置文件列名 |
ColumnsMap |
获取或设置列名映射,用于将列名映射到该列的索引 |
针对不同的测试档,可以写一个派生类继承此类。
CsvDetail
用来存放解析后的 csv 数据,支持存入到 Dictionary
或是 Matrix
中。
Property | Description |
---|---|
BodyInfo |
获取或设置测试档中的数据,以 Dictionary 形式存储 |
BodyInfo_Matrix |
获取或设置测试档中的数据,以矩阵形式存储 |
DataType |
获取或设置数据存储的形式,Dictionary 或 Matrix |
XMax |
获取或设置晶圆测试档中横坐标的最大值 |
XMin |
获取或设置晶圆测试档中横坐标的最小值 |
YMax |
获取或设置晶圆测试档中纵坐标的最大值 |
YMin |
获取或设置晶圆测试档中纵坐标的最小值 |
其中,BodyInfo
的类型是 Dictionary<Coordinate, string[]>
,以晶圆上的每个 Die 的坐标为 Key
,其 Value
按顺序存储所读取的 csv
测试档案中的电性数据。而 BodyInfo_Matrix
的类型是 string[,][]
,是一个多维交错数组,其将晶圆上每个 Die 的坐标存储在二维数组 [,]
中,而对应的电性测试数据则存储在一维数组 []
中。
针对不同的测试档,可以写一个派生类继承此类。
CsvProcessTool
用于处理 csv 文件,提供各种处理 csv 文件的方法。
Constructor | Description |
---|---|
CsvProcessTool() |
初始化一个 Coordinate 类的新实例 |
CsvProcessTool(string) |
初始化一个 Coordinate 类的新实例,并设置指定的 Pattern |
Property | Description |
---|---|
Pattern |
获取或设置正则表达式的 Pattern,默认为 “,(?=(?:[^”]*“[^]*)*[^”]*$)" |
SplitChar |
获取或设置 csv 文件的分隔符,默认为 “,” |
Method | Description |
---|---|
ReadCsvFile(string) |
读取指定的 CSV 文件, 并将文件每一行的内容根据 SplitChar 指定的分隔符进行分隔,产生的字符串数组将被放入 List 中。 |
ReadCsvFile(string, bool) |
读取指定的 CSV 文件, 第二个参数指定根据 SplitChar 指定的分隔符进行分隔,还是使用指定的正则表达式进行分隔。结果将被放入 List 中。 |
ReadCsvFileToDictionary(string, CsvTemplate, CsvDetail) |
读取指定的 CSV 文件, 并将信息存入到 CsvDetail 的 BodyInfo 属性中 |
ReadCsvFileToMatrix(string, CsvTemplate, CsvDetail) |
读取指定的 CSV 文件, 并将信息存入到 CsvDetail 的 BodyInfo_Matrix 属性中 |
ReadCsvFileAsync(string) |
ReadCsvFile(string) 的异步版本 |
GetHeaderInfoToDictionary(List<string[]>, CsvTemplate) |
提取已解析的 CSV 文件中的表头信息 |
GetBodyInfoToDictionary(List<string>[], CsvTemplate) |
提取已解析的 CSV 文件中的表身(单身)信息 |
IsInRange<T>(T, T, T) |
判断给定的 T 是否在范围内 |
IsOutOfRange<T>(T, T, T) |
判断给定的 T 是否不在范围内 |
ParseCsvLine(string, char) |
根据指定的分隔符对传入的 string 进行解析 |
ParseCsvLineByRegex(string) |
根据指定的正则表达式对传入的 string 进行解析 |
针对不同的测试档,可以写一个派生类继承此类。
晶圆图控件介绍
WaferMap
用于显示晶圆图的自定义控件。
Constructor | Description |
---|---|
WaferMap() |
初始化一个 WaferMap 类的新实例 |
Property | Description |
---|---|
XMax |
获取或设置测试档中的最大横坐标 |
XMin |
获取或设置测试档中的最小横坐标 |
YMax |
获取或设置测试档中的最大纵坐标 |
YMin |
获取或设置测试档中的最小纵坐标 |
TranslationX |
获取横坐标上的偏移量 |
TranslationY |
获取纵坐标上的偏移量 |
Zoom |
获取缩放比例 |
ScaleFactor |
获取或设置缩放系数 |
DieSize |
获取或设置绘制 Die 的尺寸 |
Detail |
获取或设置 CsvDetail |
Colors |
获取或设置 Bin 的颜色 |
DrawCross |
获取或设置是否在晶圆图上绘制十字线。 |
Field | Description |
---|---|
_waferWidth |
实际晶圆图的宽度 |
_waferHeight |
实际晶圆图的长度 |
_isDrawBin |
当前是否处于画 Bin 模式,画 Bin 模式下不允许除了描点以外的操作 |
_canModifyBin |
是否可以修改 Bin |
_squareSize |
右键固定后绘制的小正方形方框的大小 |
_opacity |
设置右键固定后出来的正方形框的透明度,0-255之间 |
_squareCenter |
右键固定后出来的正方形框的中心点 |
_fixed |
固定某一个点 |
lastX |
记录上次鼠标点击的位置 |
lastY |
记录上次鼠标点击的位置 |
drawBinPoints |
记录画 Bin 的点 |
drawBinPen |
画 Bin 时使用的画笔 |
binGraphics |
画 Bin 时使用的 Graphics 对象 |
Event | Description |
---|---|
WaferMapMouseMove |
当鼠标在控件上移动时发生 |
Method | Description |
---|---|
OnPaint(PaintEventArgs) |
重载的绘制方法,将绘制晶圆图的逻辑放在此方法中 |
WaferMap_Load(object, MouseEventArgs) |
控件Load 事件触发时调用此方法 |
WaferMap_MouseDown(object, MouseEventArgs) |
鼠标按键按下事件触发时调用此方法,用于判断拖拽动作及记录拖拽起点 |
WaferMap_MouseMove(object, MouseEventArgs) |
鼠标移动事件触发时调用此方法,用于 WaferMapMouseMove.Invoke 以及在拖动时触发重绘 |
WaferMap_MouseUp(object, MouseEventArgs) |
鼠标按键抬起事件触发时调用此方法,用于判断拖拽动作 |
WaferMap_MouseWheel(object, MouseEventArgs) |
鼠标滚轮事件触发时调用此方法,处理晶圆图的放大缩小 |
WaferMap_MouseClick(object, MouseEventArgs) |
鼠标按键点击事件触发时调用此方法,处理画 Bin 模式下的绘制及右键菜单的显示 |
RegisterEvents() |
为控件注册事件的方法 |
tsmiFixed_CheckedChanged(object, EventArgs) |
右键菜单中的固定选项的 Checked 属性的值发生更改时调用此方法,根据 Checked 的值决定是否绘制小正方形框 |
tsmiFixed_Click(object, EventArgs) |
右键菜单中的固定选项被点击时调用此方法,用于切换其 Checked 属性的值 |
RedrawWaferMap() |
令控件执行重绘,是个没什么用的方法 |
SetBinColor(Colors[]) |
根据传入的 Colors[] 设置各个 Bin 等级的颜色 |
SetBinColorDefault() |
用控件设定的默认值设置各个 Bin 等级的颜色 |
SetPosition(int, int) |
根据传入的坐标去定位 Wafer 上的某个点,将该点移至控件的中心 |
DrawBin() |
用于进入和退出画 Bin 模式 |
DrawBinUndo() |
用于画 Bin 模式下的撤销 |
CleanDrawBinHistory() |
清理画 Bin 记录 |
ModifyBin() |
画 Bin 完成后修改 Bin 值(此方法并没有完成) |
CrossProduct(Point, Point, Point) |
计算叉积 (P2 - P1) x (P3 - P1) |
IsPointOnSegment(Point, Point, Point) |
判断点 P3 是否在线段 P1P2 上(注意:调用此方法判断的前提是三点共线,即它们的叉积为 0) |
AreSegmentsIntersecting(Point, Point, Point, Point) |
判断线段 P1P2 和 P3P4 是否相交 |
IsValidPolygon(List<Points>) |
判断一组点是否构成有效的多边形 |
IsInPolygon2(Point, List<Point>) |
判断点是否在多边形内 |
GetBinColor(int) |
返回传入的 Bin 等级对应的 Color |
GetBinColor(string) |
返回传入的 Bin 等级对应的 Color |
这里对该控件的部分功能的实现做一下详细解析。
晶圆图的绘制
我选择直接在控件上的 Graphics
进行绘制,而不是使用绘制在 BitMap
上,然后以 PictureBox
作为载体进行绘制。这么做有几个原因:
BitMap
需要消耗额外内存。- 方便实现晶圆图的放大、缩小、移动等功能(坐标、偏移量等容易计算出来)。
那么要如何绘制晶圆图呢?首先要清楚控件的坐标系,是以左上角为原点 (0, 0) 的,且向右和向下分别为横坐标与纵坐标的正增量,并非我们熟知的向右和向上。其次,我们需要在控件的范围内来绘制整张晶圆图,这就涉及到绘制的比例,这个比例也很好算,“控件的大小 / 晶圆的大小”。有了比例之后,就能够算出晶圆的某个 Die 在控件上的位置了,Die 对应的横纵坐标减去晶圆的最小横纵坐标,再乘上比例,就得到了 Die 在控件上的坐标。
缩放与移动
介绍完了基本的绘制,接下来再讲讲缩放和移动这些功能是如何实现的。
先来说说移动,其实移动很简单,我们只需要知道“偏移量(Translation)”即可,举个例子会好理解很多。比如,在下面这张图中,我们将鼠标从 A 点移动到了 B 点,鼠标的移动方向是左上方,所以晶圆图的移动方向也应该是左上方,这样才符合直觉。而本次移动的“偏移量”其实就是 “B 点 - A 点”,所以本次移动在横纵坐标上的“偏移量”都是负的。那么在绘制的时候,我们就需要将偏移后的坐标作为Graphics
的原点,如图所示,偏移后的原点实际上就是当前原点 + 偏移量。为什么我们只用Graphics.TranslateTransform
改动了原点,没有改动其他代码,也能正确绘制呢? 你可以理解成我们现在所有绘制的内容都自动加上了这个偏移量。下面来看看相关代码:
1 |
|
如果侦测到了用户正在拖动鼠标,控件便会更新TranslationX
与TranslationY
,并执行重绘。在重绘方法中,将使用新的TranslationX
与TranslationY
来重设原点,并计算偏移后的坐标系上限,这样在检查到某一个 Die 的坐标超出了当前控件的坐标系范围时,可以直接跳过绘制(因为就算画了也看不见,但是资源却实实在在地消耗了)(你问为什么能画控件客户端区域外的东西?我也不知道)。
1 |
|
然后是缩放。其实缩放也很简单,缩放要处理的其实就是一个“缩放比例(Zoom)”,然后在控件重绘计算绘制比例的时候,乘以缩放比例即可。难的地方是计算缩放后的位置,因为我想做到晶圆图缩放后,鼠标指向的 Die 是同一个 Die。先从处理鼠标滚轮事件的相关代码说起吧:
1 |
|
可以发现这段代码中有个常出现的计算量:Zoom * ((float)Width / _waferWidth)
和 Zoom * ((float)Height / _waferHeight)
,可以理解为“Die 的绘制比例”,因为缩放后,每颗 Die 的尺寸也会跟着变化。稍后你会发现这个计算量同样用于重绘方法中。
保持缩放前后鼠标指向同一个 Die 的代码可能难以理解,可以结合下面的示意图来理解:
接着就是将相关参数合并到绘制方法中:
1 |
|
可以发现各个功能都挺简单的,最重要的是将他们一一拆开来实现,最后再组合在一起(然而并非简单,我再回头看都要看半天)。
WaferMapMouseMoveEventArgs
用于向其他控件传递当前鼠标所处的晶圆的坐标。
Property | Description |
---|---|
WaferX |
晶圆图横坐标 |
WaferY |
晶圆图纵坐标 |
画 Bin
由于本功能并未全部完成所以我决定先摆烂不写嘿嘿嘿~
建议直接看代码~
MiniWaferMap
相当于放大镜,用来看晶圆图的某一块区域的 Bin 分布情况。
Constructor | Description |
---|---|
MiniWaferMap() |
初始化一个 MiniWaferMap 类的新实例 |
Property | Description |
---|---|
Detail |
获取或设置 CsvDetail |
Colors |
获取或设置 Bin 的颜色 |
X |
中心 Die 在晶圆上的实际横坐标 |
Y |
中心 Die 在晶圆上的实际纵坐标 |
HalfOfTheSide |
要显示的中心点周边的 Die 的个数 |
Method | Description |
---|---|
SetBinColor() |
设置各个 Bin 等级的颜色 |
SetBinColorDefault() |
用程序默认值设置各个 Bin 等级的颜色 |
GetBinColor(int) |
返回传入的 Bin 等级对应的 Color |
GetBinColor(string) |
返回传入的 Bin 等级对应的 Color |
DrawWhitePicture(Graphics) |
绘制白布 |
DrawMiniMap(Graphics) |
绘制迷你晶圆图 |
Redraw() |
提供给外部使用,调用控件的 Invalidate() 方法进行重绘 |
你可能会在代码中发现一些额外的此处没有列出的属性或方法等,无需在意,因为那些八成是我没删掉的无用代码。
这个实现就没什么难点了,直接根据数据画图就行了。(这可比那WaferMap
好写多了!)
ElectricalMap
是一个通过 PictureBox
与 Bitmap
来展示抽/全测某个电性参数的测试数据的晶圆图的通用(并非)控件。这个时候就有吴彦祖要问了:主包主包,你不是不用 Bitmap
吗?确实,但是这抽全测看图它不需要花里胡哨的缩放和拖动功能,所以最适合展示的方式又变成了绘制 Bitmap
并用 PictureBox
进行展示。
由于赶工的原因,一些地方可能做了写死处理,不过都是小问题,图(及核心功能)没问题就是了!
Constructor | Description |
---|---|
ElectricalMap() |
初始化一个 ElectricalMap 类的新实例 |
Property | Description |
---|---|
Detail |
获取或设置 CsvDetail |
Colors |
获取或设置 Bin 的颜色 |
ElectricalMapName |
所显示的图的电性参数的名称 |
MinValue |
电性参数范围设置的最小值 |
MaxValue |
电性参数范围设置的最大值 |
StepValue |
电性参数范围设置的步长 |
DrawBorder |
是否为 Die 绘制白色边框,可以使得每个抽测点变得更清楚,不建议全测使用 |
Field | Description |
---|---|
_bitmap |
晶圆图 |
_lastMousePosition |
记录上次的鼠标位置 |
_sendMouseInfo |
是否回传坐标信息 |
Event | Description |
---|---|
ElectricalMapMouseMove |
当鼠标在控件上移动时发生 |
ElectricalMapDoubleClick |
当鼠标在控件上双击时发生 |
Method | Description |
---|---|
DrawMap(string, int, CsvDetail, int, float, int) |
绘制指定的电性参数的 Map |
DrawMiniMap(string, int, CsvDetail, int, int, int, int, int, bool) |
绘制指定的 (x,y) 旁的指定的 X 颗 Die |
LoadMap(Bitmap, string) |
直接加载传入的位图 |
GetBinColor(double) |
根据传入的值,在指定的最大最小值及步长,找到其对应的颜色并返回 |
其实画图方法没什么好讲的,我已经在代码里面用注释写得很清楚了(连公式也给你交代了!),建议直接看代码再配合画图工具理解。(其实是因为要说明的东西太多了,而且有很多相关逻辑放在了调用该控件的窗体上!另外这是后面完成的所以我开摆了。说不定什么时候心情好就再写写。)
ElectricalMapMoveEventArgs
用于向其他控件传递当前鼠标所处的晶圆的坐标。
Property | Description |
---|---|
WaferX |
晶圆图横坐标 |
WaferY |
晶圆图纵坐标 |
ElectricalMapDoubleClickEventArgs
用于向其他控件传递电性参数的名称以及对应的位图。
Property | Description |
---|---|
ElectricalName |
当前 ElectricalMap 显示的电性参数名称 |
Map |
当前 ElectricalMap 显示的晶圆图 |
两个界面的简单介绍
用户界面1(Form1
)
用来读档案的界面。单独写一个界面只是为了方便自己测试什么的。
用户界面2(WaferMapDisplayForm
)
展示 AOI 图的。
实际看图效果展示
AOI
读档完成并绘制完毕后的画面:
放大和拖动效果:
可以放得很大很大!
画 Bin
目前只是简单地完成了多边形的判断已经写死的改 Bin 方法。点击 Draw Bin(Start)
就可以开始绘制多边形了,同时按钮会变为 Draw Bin(End)
,Draw Bin Undo
与 Draw Bin Clean
按钮分别提供了撤销与清除功能。绘制完毕后,点击 Draw Bin(End)
按钮,会自动判断是否能够形成多边形并连接初始点与最后一个点。
接着点击 Modify Bin
即可改 Bin。
全测
九宫格
大图,全测档案右下角会有放大镜(迷你晶圆图)
抽测
九宫格
大图
展望
这部分主要记录想要实现的其他功能。
- 改一下在控件中绘制晶圆的区域,目前是占满了整个控件,看看能不能改成可以手动设定什么的。
- 电性资料的显示。
- 描 Bin 功能并没有做完(说是没做完实际上核心功能如描点已经完成了,只剩下修改 Bin 的部分没完成罢了)。
- 设置跟随鼠标的半透明正方形,以方便确认🔍的位置。(直接在原有的
Graphics
上进行绘制并不方便,因为貌似无法进行局部重绘…)(Ok 我放弃了 由于Winform
的渲染机制无论如何都无法避免重绘整个控件 所以我决定改成右键点击固定的时候显示)
其他想说的:项目结构管理得也太烂了,为什么我当时把所有东西都写在一个 Project 里了我请问了?拆都难拆。还有就是这代码写的也太烂了,回过头一看设计得是真烂,感觉还是过于耦合了,而且可扩展性并没有看上去的那么高。为什么这么多东西出不来一个接口?好吧实际上是因为我想到什么就写什么而且也并不清楚用户的实际需求导致最后实际需求与我所做的偏差有点大但是这东西不应该本身就由开发者来定制规范吗kuso。罚重新读一遍框架设计指南!
希望本文章能够帮到您~