SnapWindows - StickyWindows 重载






4.92/5 (8投票s)
二维几何计算的新方法取代了Point、Size和Rectangle。
引言
在我之前的文章 SizerPanel and CaptionPanel 中,我曾提到`System.Drawing.Point`、`.Size` 和 `.Rectangle` 总是迫使我写两次相同的代码:先是水平方向,然后是垂直方向,这让我感到多么烦恼。我还曾提到我编写的 `Vector` 结构,用来取代 `System.Drawing.Point` 和 `System.Drawing.Size`。我还扩展了我的几何相关内容,用一个 `Rect` 结构来取代 `Rectangle`。
其基本思想是让水平和垂直访问被索引化:索引0访问水平,索引1访问垂直。这样我就可以只写一个方向的几何代码,而另一个方向可以通过交换索引0和1来覆盖。
因此,我的首要目标不是创建一个 StickyWindows 的重制版,而是测试、证明和改进我的几何方法的强大之处。我选择了 StickyWindows 作为主题,是因为我喜欢它的理念,也喜欢其实现挑战,尤其是几何部分并非易事(在原版中,纯粹的几何计算代码约有250行)。它是让我掌握几何计算并尝试我的“武器”的一个绝佳的“陪练”。
粘滞/吸附窗口的作用
它们使窗口表现出一种“磁性行为”:当你调整窗口大小或移动窗口时,如果它靠近另一个窗口,其边缘就会吸附到另一个窗口的边缘。这确实使得在屏幕上以清晰的方式排列多个窗口变得更加容易。
实现挑战
重写 Form
你无法使用 `Resize` 或 `SizeChanged` 事件来执行窗口吸附,因为这涉及到在 `SizeChanged` 事件本身中更改大小。我的第一次尝试就是这样,结果导致了一些令人费解的行为。你也无法获得 `MouseMove` 事件,因为一个 `Form` 只有在击中*非客户区*时才会被调整大小,而此时 `MouseMove` 是不可用的。所以你必须在数据到达 .NET 管理级别之前访问它——换句话说:你必须重写 `Form`。为了实现这一点,.NET 提供了两种方法:一种简单的方法是覆盖每个想要表现为 `SnapWindow` 的 `Form` 中的 `WndProc` 方法。第二种方法是实例化一个 `NativeWindow`,将其与目标 `Form` 的句柄关联,并重写 `NativeWindow` 的 `WndProc()`。`WndProc()` 将处理所有发送给目标 `Form` 的窗口消息。我仍然认为,正如我在之前的文章中所说的那样,这是更灵活的方法,因为你不需要修改每个 `Form` 的用户代码,而只需从外部将一个 `Form` 注册为 `SnapWindow`。
现在,看看重写代码
Protected Overrides Sub WndProc(ByRef m As Message)
Const WM_NCLBUTTONDOWN As Integer = 161
Const WM_MOVING As Integer = &H216
Const WM_SIZING As Integer = &H214
Select Case m.Msg
Case WM_NCLBUTTONDOWN
'init a VirtualForm
Dim snapRects As New List(Of Rect)
_VForm = VirtualForm.TryCreate(_Form, m.WParam.ToInt32, snapRects)
If _VForm.Null Then Exit Select
'...
'... set up snapRects
'...
Case WM_MOVING, WM_SIZING
'write the computed Rect to LParam
Marshal.StructureToPtr(_VForm.GetSnapFormRect, m.LParam, False)
End Select
MyBase.WndProc(m)
End Sub
正如常言所说:“知道怎么做就很简单……” ;) - 在这种情况下,我们需要知道如何处理窗口消息的成员。
m.Msg
指示要执行的事件类型:WM_NCLBUTTONDOWN
- **W**indow **M**essage 关于 **N**on-**C**lient-area-**L**eft-**B**utton**D**own - 表示 `Form` 移动或调整大小的开始。WM_MOVING
和 WM_SIZING
不言而喻,不是吗?
当窗体边界开始改变时(WM_NCLBUTTONDOWN
),我会初始化我的 `VirtualForm` 类的实例,向其传递三个参数:`Form`、`snapRects`(一个可以吸附的 `Rectangle` 列表)和窗口消息的 `.WParam` - 呃?
m.WParam
- 这比 `m.Msg` 更棘手。通常,`WParam` 可以表示任何东西。但在与 `WM_NCLBUTTONDOWN` 消息结合使用时,它传递以下 `HT`(Hit-Test)常量之一:HTCAPTION
、HTLEFT
、HTRIGHT
、HTTOP
、HTTOPLEFT
、HTTOPRIGHT
、HTBOTTOM
、HTBOTTOMLEFT
、HTBOTTOMRIGHT
(以及其他一些不相关的常量,`TryCreate()` 会在存在时拒绝它们)。现在,`VirtualForm` 确切地知道请求的是哪种边界更改。(你可能已经猜到了:`VirtualForm` 被设计用来计算吸附的窗口边界)。
好了,下次我们遇到 `VirtualForm` 是在移动或调整大小时——代码注释已经提到了这一点:计算结果被写入 `LParam` 指针——**所有的窗口边界更改任务都完成了!** 你看
m.LParam
很棘手:与移动/调整大小消息结合使用时,它*是一个*指向定义更改后窗口边界的 `RECT` 结构的指针。并且,更改 `RECT` 结构会更改边界。
此时,直接访问了非托管代码——由于 `LParam` 是一个 `IntPtr`,你不能简单地将一个 `Rectangle` 分配给它。你必须将 `Rect` 结构按字节复制到指针指向的内存位置。这正是 `Marshal.StructureToPtr()` 为我们做的。我们希望 WinAPI 在内存中的数据结构与你提交的结构兼容。信不信由你——我的 `Rect` 结构*确实*兼容;)。
请注意,WinAPI 的 `RECT` 结构与 `System.Drawing.Rectangle` 结构不同。`Drawing.Rectangle` 使用一个位置点和一个大小来定义一个矩形,而 WinAPI 的 `RECT` 定义的是左上点(等于位置)和*右下点*(不等于 `Size`)。例如,我发现以“API方式”看待这些事物更有用,这就是为什么我的 `Rect` 结构比 `Drawing.Rectangle` 更适合 WinAPI 的原因。
我是从哪里得知重写技巧的?
(老实说,我是在网上找到的。)一种更系统的方法是打开帮助索引(离线 MSDN),将过滤器设置为“Platform SDK”,然后在搜索框中键入 `"WM_MOVING"`、`"WM_SIZING"` 或 `"WM_NCLBUTTONDOWN"`。这会带来关于这些常量以及如何使用它们的有用文章。
不幸的是,常量的值在任何地方都没有提到。你可以下载 VS2008 Platform SDK。它会安装在你的“Program Files”目录中,你会在系统中找到“*C:\Program Files\Microsoft SDKs\Windows\v6.0A\Include\WinUser.h*”,其中(除其他外)定义了*user32.dll*的所有常量。
不幸的是,对于 Express 版本,Platform SDK 没有帮助文档(尽管对此不太确定)。
Vector 和 Rect
Vector 是 Size 和 Point 的结合。它提供了 `X`、`Y` 和执行简单二维几何数学的常用功能。我做了一系列 `CType` 运算符,使其尽可能灵活和兼容。它的特殊功能是你可以通过使用 `Integer` 索引来访问 `X` 和 `Y`,以避免编写每段几何代码的两个变体。
Rect
只是两个 `Vector`。如前所述,它不将描述的矩形定义为 Point 和 Size 的组合,而是定义左上点和右下点。我称它们为 `.Near` 和 `.Far`,以向 `System.Text.StringAlignment` 枚举致敬,这是我所知的框架中唯一一个也适用于两个维度的项(你可以通过设置 `StringFormat.Alignment.Near/.Center/.Far` 来水平对齐文本绘制,并通过设置 `StringFormat.LineAlignment.Near/.Center/.Far` 来垂直对齐)。在接下来的部分中,我将使用术语Near或Far来区分顶部或左侧实体与底部或右侧实体。当然,`Near` 和 `Far` 也可以通过索引访问。
VirtualForm
它只在构造函数中获取一次数据:`Rects` 和 `Vectors`。当请求新的窗口边界时,它首先计算正常的窗口边界,仅使用全局可访问的 `Control.MousePosition` 作为附加数据。第二步,将所有“吸附候选矩形”与计算出的正常窗口边界进行比较,距离最小的边缘将被吸附。这对于水平边缘和垂直边缘都这样做。
VirtualForm 必须在不依赖于实际窗口的情况下计算 `NormalFormRect`,因为当另一个窗口的边缘被吸附时,实际边界是不同的。
当 `NormalFormRect` 与吸附边缘之间的距离超过“磁性距离”时,必须知道 `NormalFormRect` 才能执行“吸附关闭”。
因此,我们首先必须理解 `NormalFormRect` 是如何计算的,也就是说:理解鼠标是如何移动或调整窗口大小的。
实际上,只要你稍微更普遍地理解这个过程,移动窗口和调整窗口大小就没有区别。你会发现这总是通过拖动边缘来完成的:你可以拖动一个边缘(鼠标抓住窗口边缘)、两个边缘(鼠标抓住一个角)或四个边缘(鼠标抓住标题栏)。(拖动*三个*边缘是理论上的选项,但基础架构不提供)。
边缘拖动必须通过存储关于哪些边缘受到影响的信息,以及存储“抓取偏移量”,即受影响边缘到鼠标位置的距离来初始化。VirtualForm 构造函数用两行代码完成此操作。
_SizeType = HT2SizeOption(HT)
_GrabOffs = Rect.From(Control.MousePosition).Subtract(_FormRect)
(为了代码的简洁,`_GrabOffs` 存储*所有*边缘距离,尽管只需要受影响的那些。)
现在,它可以在请求时重新计算 NormalFormBounds,因为拖动原理很简单:*始终保持偏移量不变*。但重新计算并非完全简单,因为我不能更改所有边缘,并且必须根据 `_SizeType` 来区分边缘。
为了深入理解我如何实现这种区分,让我做一个分叉,关于...
枚举的进阶
枚举可用于存储和查询多个布尔语句。使用按位运算符 `Or`、`And` 和 `Xor`,你可以用一个命令过滤和修改(`Int32`)枚举成员的全部32个布尔语句。`VirtualForm` 构造函数做的第一件事之一就是调用 `HT2SizeOption()` 将传递的 `HT` 常量映射到一个名为 `SizeOption` 的枚举。
Public Enum SizeOption As Integer
Top = 1
Right = 2
Bottom = 4
Left = 8
End Enum
为了代码简洁,我还添加了主要使用的位组合,因此完整的定义如下:
Public Enum SizeOption As Integer
Top = 1
Right = 2
Bottom = 4
Left = 8
TopLeft = Top Or Left
TopRight = Top Or Right
BottomLeft = Bottom Or Left
BottomRight = Bottom Or Right
Caption = Top Or Left Or Bottom Or Right
End Enum
没什么高深莫测的,对吧?它清楚地定义了拖动窗口的九种选项。
现在你可以检查一个给定的(多个)布尔语句(由枚举值表示)是否与另一个给定的布尔语句具有一个或多个共同的位。
(_SizeType And SizeOption.Top) = SizeOption.Top
如果 `_SizeType` 是以下之一,则匹配:`Top`、`TopLeft`、`TopRight`、`Caption`。
另一种查询方式是:
(_SizeType And SizeOption.TopLeft) <> 0
如果*任何* `_SizeType` 位与*任何* `TopLeft` 位匹配,则此匹配。特别是:`Top`、`Left`、`TopLeft`、`TopRight`、`BottomLeft`、`Caption`。
如果你查询一个单比特语句,与0的比较代码比第一种变体短。
(_SizeType And SizeOption.Top) = SizeOption.Top
得到与...相同的结果:
(_SizeType And SizeOption.Top) = 0
回到矩形计算
可索引的结构也意味着我可以迭代它们。在 `Rect` 的情况下,我可以进行两种不同的迭代:从近到远,以及从水平到垂直。我可以嵌套两种方式,最终得到一个*二维迭代*。
'(Dim rct As Rect)
For far = 0 To 1
For vertical = 0 To 1
Dim edge As Integer = rct(far, vertical)
Next
Next
这以以下顺序循环四个边缘:`Left`、`Top`、`Right`、`Bottom`。现在,我构建四个*“枚举查询”*,可以以相同的方式迭代它们:我构建一个*二维* `Array` 的 Enum
。
Private Shared _SizeOptions As SizeOption(,) = New SizeOption(,) { _
{SizeOption.Left, SizeOption.Top}, _
{SizeOption.Right, SizeOption.Bottom}}
现在我可以在 2D 循环中迭代 `_SizeOptions`、`_FormRect` 的边缘和 `_GrabOffset`,选择受影响的边缘,并分配值,使到鼠标位置的距离等于相应的偏移量(拖动原理)。
Private Sub ComputeFormRect()
Dim ptMouse = Vector.From(Control.MousePosition)
For far = 0 To 1
For vertical = 0 To 1
If 0 <> (_SizeType And _SizeOptions(far, vertical)) Then
_FormRect(far, vertical) = ptMouse(vertical) - _GrabOffs(far, vertical)
End If
Next
Next
End Sub
计算 SnapRect
整个故事
Public Function GetSnapFormRect() As Rect
ComputeFormRect()
Dim result = _FormRect
Dim snapSize = Vector.From(SnapWindow.SnapSize)
Dim inflated = _FormRect.Add(-snapSize, snapSize)
For vertical = 0 To 1
Dim horizontal = vertical Xor 1
'get min-distance between _FormRects dragged
'edges and any edge of any _SnapRect
Dim minDist = Integer.MaxValue
For far As Integer = 0 To 1
'filter dragged edge(s)
If 0 = (_SizeType And _SizeOptions(far, vertical)) Then Continue For
For Each rct2 In _SnapRects.Where( _
Function(r2) Intersect1D(r2, inflated, horizontal))
For far2 As Integer = 0 To 1
Dim distance = _FormRect(far, vertical) - rct2(far2, vertical)
If distance.Abs() < minDist.Abs() Then minDist = distance
Next
Next
Next
If minDist.Abs() > SnapWindow.SnapSize Then Continue For
For far = 0 To 1
'filter dragged edges
If 0 <> (_SizeType And _SizeOptions(far, vertical)) Then _
result(far, vertical) -= minDist
Next
Next
If _SizeType <> SizeOption.Caption Then
'clip near-edges (WinApi cares for the far-edge-restriction)
result.Near = result.Near.Intersect(_NearClip.Far).Union(_NearClip.Near)
End If
Return result
End Function
现在我们将逐步检查它
ComputeFormRect()
Dim result = _FormRect
Dim snapSize = Vector.From(SnapWindow.SnapSize)
Dim inflated = _FormRect.Add(-snapSize, snapSize)
并不复杂:计算正常的 `_FormRect`(一个类成员)并将其设为默认返回值。然后,创建 `inflated`,作为 `_FormRect`,通过磁性距离进行膨胀。
然后,进入二维矩形循环。
For vertical = 0 To 1
Dim horizontal = vertical Xor 1
Dim minDist = Integer.MaxValue
For far As Integer = 0 To 1
'...
注意第二行:Dim horizontal = vertical Xor 1
。 `valueToSwitch Xor 1` - 从 0 切换到 1 并反转的最简单方法。
现在我们来到核心。首先,如果被询问的 `_FormRect` 边缘根本不可移动,则跳过计算(根据 `_SizeType` 进行检查)。
If (_SizeType And _SizeOptions(far, vertical)) = 0 Then Continue For
如果 `_SizeType` 与 `(far, vertical)` 寻址的 `_SizeOption` 匹配,我们循环 `_SnapRect` 以搜索到 `(far, vertical)` 寻址的 `_FormRect` 边缘的最小距离边缘。
For Each rct2 In _SnapRects.Where(Function(r2) Intersect1D(r2, inflated, horizontal))
For far2 As Integer = 0 To 1
Dim distance = _FormRect(far, vertical) - rct2(far2, vertical)
If distance.Abs() < minDist.Abs() Then minDist = distance
Next
Next
再次,有一个过滤器:SnapRects.Where(Function(r2) Intersect1D(r2, inflated, horizontal))
只选择那些在*另一个方向*与 `inflated` 窗口矩形(前面提到的)*相交*的 `Rect` 进行检查。因为,当它位于大约 500 像素下方时,将边缘向左吸附 15 像素是没有意义的;)。**注意**:这里,我对相交有一维的理解:一个矩形可能在水平方向上相交,但在垂直方向上不相交(例如,一行中的单词在水平方向上相交,但在垂直方向上不相交)。请参阅相交计算代码。
Private Function Intersect1D(ByVal rct1 As Rect, _
ByVal rct2 As Rect, ByVal vertical As Integer) As Boolean
Return rct1.Far(vertical) >= rct2.Near(vertical) _
AndAlso rct2.Far(vertical) >= rct1.Near(vertical)
End Function
好的,现在我们有了 `minDist`,并且可以将其应用于我们的返回结果(如果 `minDist` 足够小)。
If minDist.Abs() > SnapWindow.SnapSize Then Continue For
For far = 0 To 1
'apply minDist only to dragged edges
If 0 <> (_SizeType And _SizeOptions(far, vertical)) Then _
result(far, vertical) -= minDist
Next
接着是(我希望如此)最后一个惊喜。
If _SizeType <> SizeOption.Caption Then
'clip near-edges (WinApi cares for the far-edge-restriction)
result.Near = result.Near.Intersect(_NearClip.Far).Union(_NearClip.Near)
End If
呃?为什么 WinAPI 只关心远边缘限制,而不关心近边缘限制?“关心边缘限制”到底意味着什么?为了说明(也为了你的乐趣),我创建了一个小演示,展示了当你注释掉最后的剪切时会发生什么。
明白了?违反上面的 `MinimumSize` 通过将窗口的 `Height` 设置回最小值来防止。但是改变的是*顶部*!——因此窗口*移动*了,尽管你实际上在*调整*顶部边缘的大小。
还有一些未解决的要点
Vector.Intersect() 和 Vector.Union()
让我们从前面提到的剪切开始。
result.Near = result.Near.Intersect(_NearClip.Far).Union(_NearClip.Near)
Intersect()
和 Union()
应用于 `Vector`,而不是 `Rect`!`Vector` 如何与另一个 `Vector` 相交,它没有大小!这就是重点。由于 `Vector` 代表一个点和一个大小,它*就是*一个大小。当我相交两个 `Vector` 时,我将它们视为两个 `Rect`,其中 `.`Near-Vector`= (0,0)`. 从另一个角度看:`Vector1.Intersect(Vector2)` 在每个方向上返回两者的最小值。而这正是将 `Vector` 剪裁到 `Rect` 内部的*一个*(上限)部分。剪裁的*另一个*(下限)部分是与 `Rect` 的 `.`Near-Vector` 的 `Union`,这意味着在每个方向上的*最大值*。总之:你可以通过将 `union(v, rct.Near)` 与 `rct.Far` *相交*来将 `Vector v` 剪裁到 `Rect rct` 内部。
Rect 的相交方式
System.Drawing.Rectangle.Intersect(other As Rectangle)
如果 `Rectangle` 不相交,则返回 `Rectangle.Empty`。相反,`Rect.Intersect()` 的计算如下:
''' <summary>
''' a none-intersection is indicated by returning an imaginary Rect
''' </summary>
Public Function Intersect(ByVal other As Rect) As Rect
For i = 0 To 1
Intersect.Near(i) = Near(i).Max(other.Near(i))
Intersect.Far(i) = Far(i).Min(other.Far(i))
Next
End Function
''' <summary>true, if my size has a negative component</summary>
Public ReadOnly Property IsImaginary() As Boolean
Get
If Far.X - Near.X < 0 Then Return True
Return Far.Y - Near.Y < 0
End Get
End Property
优点是,一个想象中的 `Rect` 仍然可以提供有价值的信息:即两个 `Rect` 之间的*距离*。明白了?*两个矩形之间的距离*被理解为*负相交*。
“同质结构”的思想
字段仅为一种数据类型的结构,我称它们为*“同质”*。在我看来,我们可以对它们应用一些原则,例如我已解释过的索引性。
通常,你可以尝试将单元类型的功能转移到整个结构。
求反运算符
由于 Integer
是可求反的,我希望 `Vector` 和 `Rect` 也支持它。
Public Shared Operator -(ByVal v1 As Vector) As Vector
Return New Vector(-v1.X, -v1.Y)
End Operator
Public Shared Operator -(ByVal r As Rect) As Rect
Return New Rect(-r.Near, -r.Far)
End Operator
数据拉伸
这意味着可能有几个操作适用,尽管可能提供的数据不足以显式分配给每个成员。
例如,`.From()` 操作。我将其视为一个公共共享构造函数,比旧的 `New()` 更方便一些。
Dim v = Vector.From(x As Integer, y As Integer)
这是基本的。但以下方法也同样清晰:
Dim v = Vector.From(amount As Integer)
这只能意味着将 `amount` 分配给 `v.X` 和 `v.Y`。
在这里看到一个数据拉伸相等比较。
Dim b = v.EachEquals(amount As Integer)
这很方便:突然,我有了一种“推断常量”(由 Integer
推断)。这意味着 `Vector.From(0)` 是 `Point.Empty` 的完美替代品。而且我可以自由定义其他“特殊点”,例如 `Vector.From(Integer.MaxValue)` 或 `Vector.From(Integer.MinValue)`,以(按约定)指示无效位置,而无需使用 Nullable(Of T)
结构。
通过 `Vector.EachEquals()` 覆盖与每个特殊点的比较,例如 `Vector.EachEquals(0)` 替代 `Point.IsEmpty`,而 `Vector.EachEquals(Integer.MinValue)` 是一个新功能,在 `Point` 或 `Size` 中没有等效项,也许最多是*非常*丑陋的。
Dim pt = New Point(Integer.MinValue, Integer.MinValue)
Dim b = pt = New Point(Integer.MinValue, Integer.MinValue)
继续拉伸数据。
理论上,我可以如下重载 `+` 运算符:
Public Shared Operator +(ByVal v As Vector, ByVal amount As Integer) As Vector
Return New Vector(v.X + amount, v.Y + amount)
End Operator
用表达式使观众困惑,例如:
Dim v = Vector.From(4, 5) + 9
但这对我来说也太不顾及他人了。 ;) 相反,看看我的 `Add()` 重载,第二个执行数据拉伸。
Public Function Add(ByVal x As Integer, ByVal y As Integer) As Vector
Return New Vector(Me.X + x, Me.Y + y)
End Function
Public Function Add(ByVal amount As Integer) As Vector
Return New Vector(X + amount, Y + amount)
End Function
Public Function Add(ByVal v As Vector) As Vector
Return New Vector(X + v.X, Y + v.Y)
End Function
这会执行偏移、放大/缩小,并且由于一些类型提升的 `CType()` 运算符,它与 `Point` 和 `Size` 兼容。(`.Subtract()` 的工作方式相同。)
Rect 的数据拉伸
`.From()` 和 `.EachEquals()` 的优势当然也存在,而在 `.From()` 的情况下,我还支持一个额外的拉伸选项。
Public Shared Function From(ByVal eachMember As Integer) As Rect
Return Rect.From(Vector.From(eachMember))
End Function
Public Shared Function From(ByVal eachMember As Vector) As Rect
Return New Rect(eachMember, eachMember)
End Function
`.Add()` 也变得更有趣。
Public Function Add(ByVal toEach As Integer) As Rect
Return Add(Vector.From(toEach))
End Function
''' <summary>performs offset </summary>
Public Function Add(ByVal toEach As Vector) As Rect
Return New Rect(Near + toEach, Far + toEach)
End Function
''' <summary>
''' performs sophisticated offsets, eg. inflating/deflating (if v1 = -v2)
''' </summary>
Public Function Add(ByVal v1 As Vector, ByVal v2 As Vector) As Rect
Return New Rect(Near + v1, Far + v2)
End Function
''' <summary>
''' performs sophisticated offsets, supports compatiblity to Rectangle
''' </summary>
Public Function Add(ByVal r As Rect) As Rect
Return New Rect(Near + r.Near, Far + r.Far)
End Function
(`.Subtract()` 依此类推。)
上述代码在后台运行,当 VirtualForm 用一行代码获取四边偏移量时。
_GrabOffs = Rect.From(Control.MousePosition).Subtract(_FormRect)
或者当我用 `snapSize` 膨胀 `_FormRect` 时。
Dim inflated = _FormRect.Add(-snapSize, snapSize)
LINQ
''' <summary>enables Linq-power to Vectors Elements</summary>
Public Function GetEnumerator() As IEnumerator(Of Integer) Implements _
IEnumerable(Of Integer).GetEnumerator
Return DirectCast(New Integer() {X, Y}, IEnumerable(Of Integer)) _
.GetEnumerator
End Function
Private Function GetEnumerator1() As IEnumerator Implements _
IEnumerable.GetEnumerator
Return DirectCast(New Integer() {X, Y}, IEnumerable(Of Integer)) _
.GetEnumerator
End Function
''' <summary>enables Linq-power to Rects elements</summary>
Public Function GetEnumerator() As IEnumerator(Of Vector) Implements _
IEnumerable(Of Vector).GetEnumerator
Return DirectCast(New Vector() {Near, Far}, IEnumerable(Of Vector)) _
.GetEnumerator
End Function
Private Function GetEnumerator1() As IEnumerator Implements _
IEnumerable.GetEnumerator
Return DirectCast(New Vector() {Near, Far}, IEnumerable(Of Vector)) _
.GetEnumerator
End Function
我不知道这是否有用。我个人不使用它。但它对我来说看起来很有趣,而且很容易实现。 ;)
用扩展函数替换公共共享函数
这有点离题,但我想再提一个原则,让你理解所有展示的代码。
当涉及到扩展函数时,在很多情况下,不再需要 Public Shared
函数,这些函数迫使你输入完整的限定符来调用它们。比较这些剪切变体:
Public Function ClipIn(ByVal pt As Point, _
ByVal rct As Rectangle) As Point
Return New Point( _
Math.Max(Math.Min(pt.X, rct.Right), rct.X), _
Math.Max(Math.Min(pt.Y, rct.Bottom), rct.Y))
End Function
Public Function ClipIn2(ByVal pt As Point, _
ByVal rct As Rectangle) As Point
Return New Point(pt.X.Max(rct.X).Min(rct.Right), _
pt.Y.Max(rct.Y).Min(rct.Bottom))
End Function
在第二种变体中,`Min()/Max()` 显示为一个扩展函数,它更短、更容易输入(得益于 intellisense),并且将函数棘手的嵌套解析为更易于处理的串联。我将相同的概念应用于 `Math.Abs()`(也许你已经注意到了,看了我之前的代码示例)。
即使是检查 Nothing
,我也更喜欢将其作为扩展。
<Extension()> _
Public Function Null(Of T As Class)(ByVal Subj As T) As Boolean
Return Subj Is Nothing
End Function
<Extension()> _
Public Function NotNull(Of T As Class)(ByVal Subj As T) As Boolean
Return Subj IsNot Nothing
End Function
更多想法 - 三维
我并不真正了解 DirectX,但我很想知道 DirectX 开发者是如何不发疯的。他们难道不被迫写三遍代码吗?或者更糟,2.Pow(2)
次?我不知道复杂性随着维度的增加而确切增加多少,但棋盘上的一个正方形有 8 个邻居,而一个紧凑的立方体则接触 26 个邻居!
我想知道*“StickyCubes”*应用程序看起来怎么样? ;)