Introduction
In my previous article SizerPanel and CaptionPanel, I had mentioned how it annoys me that System.Drawing.Point
, .Size
, and .Rectangle
always force me to write the same code twice: first for the horizontal direction, and then for the vertical. And I had also mentioned about the structure Vector
that I wrote to replace System.Drawing.Point
as well as System.Drawing.Size
. I also extended my geometric stuff with a Rect
structure to replace Rectangle
.
The basic idea of this is to make horizontal and vertical access indexed: index 0 accesses horizontal, index 1 vertical. Now I can write geometric code for only one direction, and the other will be covered by swapping the indices 0 and 1.
So my primary goal was not to create a remake of StickyWindows, but to test, proof, and improve the power of my geometric approach. I took StickyWindows as the topic because I liked the idea, and also the implementation-challenge, especially the geometric part is not that trivial (in the original, the pure geometric computing-code took about 250 lines). It's an excellent "sparring-partner" to become fit in geometric computations, and to try out my "weapons".
What sticky/snap windows do
They make forms perform a kind of "magnetic behavior": when you resize or move a form, and it gets close enough to another form, the near edge snaps to the edge of the other. That really makes it easier to arrange multiple forms on screen in a clear manner.
Implementation challenges
Subclass the Form
You can't use the Resize
or SizeChanged
event to perform form-snapping, because that implies changing the size within the SizeChanged
event itself. My first trials were in that direction, and they ended up in some real puzzling behavior. Nor will you get MouseMove
events, because a Form
is just resized, when especially the non-client-area is hit, and there is no MouseMove
available. So you must access the required data before it appears on the .NET-managed level - in other words: you must subclass the Form
. To achieve this, .NET provides two ways: the simpler one is to override the WndProc
Sub in each Form
you want to behave as a SnapWindow
. The second way is to instantiate a NativeWindow
, assign the target form's handle, and override the NativeWindow
's WndProc()
. The WndProc()
will handle all the window messages to the target form. And I still believe as in my previous article, that this is the more flexible way, because you don't need to modify each form's user code, but can simply register a Form
as a SnapWindow
, from the outside.
Now, take a look at the subclassing code:
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
Dim snapRects As New List(Of Rect)
_VForm = VirtualForm.TryCreate(_Form, m.WParam.ToInt32, snapRects)
If _VForm.Null Then Exit Select
Case WM_MOVING, WM_SIZING
Marshal.StructureToPtr(_VForm.GetSnapFormRect, m.LParam, False)
End Select
MyBase.WndProc(m)
End Sub
As it often happens: It's easy when you know how... ;) - in this case, we need to know how to handle the members of the window message:
m.Msg
indicates the type of event to perform: WM_NCLBUTTONDOWN
- WindowMessage about a Non-Client-area-Left-ButtonDown - indicates the beginning of a Form
move or resize. WM_MOVING
, and WM_SIZING
are self-explanatory, aren't they?
When the form-bounds start to change (WM_NCLBUTTONDOWN
), I initialize an instance of my VirtualForm
class, passing three things to it: the Form
, the snapRects
(a list of Rectangle
s which can be snapped), and the window message's .WParam
- uh?
m.WParam
- this is more tricky than m.Msg
. In general, the WParam
can mean everything. But in combination with the WM_NCLBUTTONDOWN
message, it passes one of the following HT
(Hit-Test) constants: HTCAPTION
, HTLEFT
, HTRIGHT
, HTTOP
, HTTOPLEFT
, HTTOPRIGHT
, HTBOTTOM
, HTBOTTOMLEFT
, HTBOTTOMRIGHT
(and some other, which are out of interest - TryCreate()
rejects them, if they are there). Now, the VirtualForm
knows exactly what kind of bounds-changing is requested. (You will already suspect: VirtualForm
is designed to compute snapping form-bounds).
Well, and the next time we meet our VirtualForm
is when a moving or a sizing occurs - and the code-comment already mentions this: the computing result is written to the LParam
pointer - and all of the form-bounds-change-task is done! You see:
m.LParam
is tricky: combined with a move/size message, it is a pointer which points to the RECT
structure which defines the form-bounds after the change. And, changing the RECT
structure changes the bounds.
At this point, unmanaged code is directly accessed - since LParam
is an IntPtr
, you cannot simply assign a Rectangle
to it. You must copy the Rect
structure byte wise to the place in memory where the pointer points to. This does Marshal.StructureToPtr()
for us. And we hope that the data structure the WinAPI expects at that place in memory is compatible with the structure you submitted. Believe it or not - my Rect
structure is compatible ;).
Note that the WinAPI RECT
structure is different to the System.Drawing.Rectangle
structure. Drawing.Rectangle
defines a rectangle by a location-point and a size, while the WinAPI RECT
defines the top-left point (equals location) and the bottom-right point (not equal to Size
). I, for instance, found it more useful to see those things the "API-way", and that's why my Rect
structure fits better to WinAPI than Drawing.Rectangle
does.
From where did I get to know about the subclassing stuff?
(To be honest, I picked it up from the internet.) A more systematic method to find the info would be to open the Help-Index (offline-MSDN), set the filter to "Platform SDK", and type "WM_MOVING"
, "WM_SIZING"
or "WM_NCLBUTTONDOWN"
in the search-textbox. This brings up informative articles about these constants, and how you can work with them.
Unfortunately, the values of the constants aren't mentioned anywhere. You can download the VS2008 Platform SDK. This will install in your Program Files, and you will find a 'C:\Program Files\Microsoft SDKs\Windows\v6.0A\Include\WinUser.h' on your system, where (among other things) all constants of the user32.dll are defined.
Unfortunately, Help for the "Platform SDK" isn't available for Express editions (not sure about this, though).
Vector and Rect
A Vector is a Size and a Point in one. It provides X
, Y
and usual features to perform simple 2D-geometric math. I made a bunch of CType
operators to make it as flexible and compatible as possible. Its special feature is that you can access X
and Y
by using an Integer
index in order to avoid writing each piece of geometric code in two variants.
Rect
simply is two Vector
s. As I mentioned before, it doesn't define the described rectangle as a composition of a Point and a Size, but defines the top-left Point and the bottom-right one. I call them .Near
and .Far
, as a homage to the System.Text.StringAlignment
enumeration, which is the only item I know in the Framework which also works in both dimensions (you can align a text-drawing horizontally by setting StringFormat.Alignment.Near/.Center/.Far
, and align it vertically by setting StringFormat.LineAlignment.Near/.Center/.Far
). In following sections, I will use the terms Near or Far to distinguish between a top or left entity from a bottom or right one. And, of course, Near
and Far
are accessible by index too.
VirtualForm
It only gets data once, in the constructor: Rects
and Vectors
. When new form-bounds are requested, it first computes the normal form-bounds, only using the globally accessible Control.MousePosition
as additional data. In the second step, all "snap-candidate-rectangles" are compared with the computed normal-form-bounds, and the edge with the minimum distance gets snapped. This is done with the horizontal edges as well as with the vertical ones.
VirtualForm must compute the NormalFormRect
without dependence to the real form, because the real bounds are different when another form's edge is snapped.
The NormalFormRect
must be known to perform the "snapping off", when the distance between the NormalFormRect
and a snapped edge exceeds the "magnetic distance".
So first, we have to understand how the NormalFormRect
is computed, that means: comprehend how a mouse moves or resizes the form.
In fact, there is no difference between moving a form and resizing it, if you understand the process a bit more in general. You will see that it is always done by dragging edges: you can drag one edge (mouse grabs a form-edge), two edges (mouse grabs a corner), or four edges (mouse grabs the caption (=title-bar)). (To drag three edges is a theoretical option, but not provided by the infrastructure.)
Edge-dragging must be initialized by storing the information about which edges are affected, and also storing the "Grab Offset", which is the distances of the affected edges to the mouse-position. The VirtualForm constructor does this in two lines of code:
_SizeType = HT2SizeOption(HT)
_GrabOffs = Rect.From(Control.MousePosition).Subtract(_FormRect)
(For simplicity in code, _GrabOffs
stores all edge-distances, although only the affected ones are needed.)
Now, it can recompute the NormalFormBounds, whenever requested, since the drag-principle is the simple: always keep the offset constant. But recomputing isn't quite that simple, because I'm not allowed to change all edges, and must distinguish between the edges according to _SizeType
.
To understand in depth how I achieve that distinction, let me do an egression about...
Enumeration advanced
Enumerations are useful to store and query multiple boolean statements. With the bit operators Or
, And
, and Xor
, you can filter and modify all the 32 bool-statements of an (Int32
-)enumeration-member with one command. One of the first things the VirtualForm
constructer does is call HT2SizeOption()
to map the passed HT
-constant to an enumeration named SizeOption
.
Public Enum SizeOption As Integer
Top = 1
Right = 2
Bottom = 4
Left = 8
End Enum
For brevity in code, I added the mainly used bit-combinations too so that the complete definition comes up as:
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
No rocket science, is it? And it defines clearly the nine options to drag a form.
Now you can check out whether a given (multiple) bool-statement (represented by an enumeration value) has one or more bits in common with a given other one:
(_SizeType And SizeOption.Top) = SizeOption.Top
will match, if _SizeType
is one of the following: Top
, TopLeft
, TopRight
, Caption
.
Another way to query is:
(_SizeType And SizeOption.TopLeft) <> 0
This will match, if any of the _SizeType
bits match any of the TopLeft
bits. In particular: Top
, Left
, TopLeft
, TopRight
, BottomLeft
, Caption
.
If you query against a 1-bit-statement, the code for comparison to 0 is shorter than the first variant:
(_SizeType And SizeOption.Top) = SizeOption.Top
has the same results as:
(_SizeType And SizeOption.Top) = 0
Back to rectangle-computation
Indexable structures also imply that I can iterate them. In the case of Rect
, I can iterate in two different ways: from near to far as well as from horizontal to vertical. I can nest in two ways and end up with a two-dimensional iteration.
For far = 0 To 1
For vertical = 0 To 1
Dim edge As Integer = rct(far, vertical)
Next
Next
This loops the four edges in the following order: Left
, Top
, Right
, Bottom
. And now, I build four "enumeration-queries", which can be iterated in the same way: I build a two-dimensional Array
of Enum
s:
Private Shared _SizeOptions As SizeOption(,) = New SizeOption(,) { _
{SizeOption.Left, SizeOption.Top}, _
{SizeOption.Right, SizeOption.Bottom}}
Now I can iterate _SizeOptions
, _FormRect
-edges, and _GrabOffset
s within a 2D-loop, select the affected edges, and assign values, to make the distances to the mouse position equal to the corresponding offset (principle of dragging).
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
Computing the SnapRect
The whole story:
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
Dim minDist = Integer.MaxValue
For far As Integer = 0 To 1
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
If 0 <> (_SizeType And _SizeOptions(far, vertical)) Then _
result(far, vertical) -= minDist
Next
Next
If _SizeType <> SizeOption.Caption Then
result.Near = result.Near.Intersect(_NearClip.Far).Union(_NearClip.Near)
End If
Return result
End Function
Now we will examine it step by step:
ComputeFormRect()
Dim result = _FormRect
Dim snapSize = Vector.From(SnapWindow.SnapSize)
Dim inflated = _FormRect.Add(-snapSize, snapSize)
is not that complicated: compute the normal _FormRect
(a class member) and set it as the default return value. Then, create inflated
, as _FormRect
, inflated by the magnetic distance.
Then, enter the two-dimensional rectangle-loop:
For vertical = 0 To 1
Dim horizontal = vertical Xor 1
Dim minDist = Integer.MaxValue
For far As Integer = 0 To 1
Note the second line: Dim horizontal = vertical Xor 1
. valueToSwitch Xor 1
- the easiest way to switch an index from 0 to 1 and reverse.
Now we come to the kernel. First, skip the computation if the _FormRect
edge in question isn't moveable at all (check against _SizeType
):
If (_SizeType And _SizeOptions(far, vertical)) = 0 Then Continue For
If _SizeType
matches with the (far, vertical)
addressed _SizeOption
, we loop _SnapRect
s in order to search the minimum-distance-edge to the (far, vertical)
addressed _FormRect
edge:
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
Again, there is a filter: SnapRects.Where(Function(r2) Intersect1D(r2, inflated, horizontal))
only takes those Rect
s to examine, which intersect in the other direction with the inflated
FormRect (mentioned before). Because, it doesn't make sense to snap an edge 15 pixels to the left, when it is located about 500 pixels below ;). Note: Here, I get an one-dimensional understanding of intersection: a rectangle may intersect in the horizontal direction, but not vertical (e.g., words in a line intersect horizontal, but not vertical ). See the intersection computation code:
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
OK, now we have our minDist
, and can apply it to our return-result (if minDist
is small enough):
If minDist.Abs() > SnapWindow.SnapSize Then Continue For
For far = 0 To 1
If 0 <> (_SizeType And _SizeOptions(far, vertical)) Then _
result(far, vertical) -= minDist
Next
followed by the (I hope so) last point of surprise:
If _SizeType <> SizeOption.Caption Then
result.Near = result.Near.Intersect(_NearClip.Far).Union(_NearClip.Near)
End If
Eh? Why does WinAPI only care for far-edge-restriction, but not restrict the near-edges? What does it mean at all: "caring for edge-restriction"? To illustrate (and for your pleasure), I created a little demo, to show what happens if you comment out that final clip:
Got it? Violating the MinimumSize
from above is prevented by setting the form's Height
back to the minimum amount. But it was the Top which changed! - and so the form moves, although you're actually sizing the top edge.
Some points of interest left open
Vector.Intersect() and Vector.Union()
Let's begin with the above mentioned clip:
result.Near = result.Near.Intersect(_NearClip.Far).Union(_NearClip.Near)
Intersect()
and Union()
are not applied to Rect
s, but to Vector
s! How can a Vector
intersect with another - it has no size! And that's the point. Since a Vector
represents a point as well as a size, it is a size. And when I intersect two Vector
s, I take them as two Rect
s with .Near-Vector= (0,0)
. In another view: Vector1.Intersect(Vector2)
returns the minimum amount of both in each direction. And, that's exactly the one (the ceiling-) part of clipping a Vector
inside a Rect
. The other (floor-) part of clipping is a Union
with the Rect
's .Near-Vector
, which means the maximum amount in each direction. In summary: you clip the Vector v
within the Rect rct
by Intersect
ing the union(v, rct.Near)
with rct.Far
.
Rects way of intersection
System.Drawing.Rectangle.Intersect(other As Rectangle)
returns Rectangle.Empty
, if the Rectangle
s do not intersect. Instead of that, Rect.Intersect()
is computed as follows:
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
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
The advantage is that an imaginary Rect
still delivers a valuable information: namely, the distance between both Rect
s. Got it? The distance between two rectangles is understood as negative intersection.
The idea of "homogen structure"
Structures whose fields are only of one data type, I address them as "homogen". In my opinion, we can apply some principles to them, e.g., the indexablity I already explained.
In general, you can try to transfer features from the unit-types to the whole structure.
Inverting operator
Since Integer
is invertable, I want Vector
and Rect
also to support it:
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
Data-stretching
This means several operations may be applicable, although there may be passed too few data to explicitly assign to each member.
For instance, the operation .From()
. I take it as a public shared constructor, a bit more comfortable than the good old New()
:
Dim v = Vector.From(x As Integer, y As Integer)
That is the basic one. But also unambiguous is to do this:
Dim v = Vector.From(amount As Integer)
It only can mean to assign amount
to v.X
as well as to v.Y
.
See a data stretching equality-comparison here:
Dim b = v.EachEquals(amount As Integer)
And this is convenient: Suddenly, I have a sort of "inferred constants" (inferred by Integer
). That means, Vector.From(0)
is a perfect substitute of Point.Empty
. And I'm free to define other "special-points", e.g., Vector.From(Integer.MaxValue)
or Vector.From(Integer.MinValue)
, to indicate (by convention) an invalid location without using the Nullable(Of T)
structure.
Checking against each special-point is covered by Vector.EachEquals()
, e.g., Vector.EachEquals(0)
substitutes Point.IsEmpty
, and Vector.EachEquals(Integer.MinValue)
is a new feature with no equivalent in Point
or Size
, maybe at best the really ugly:
Dim pt = New Point(Integer.MinValue, Integer.MinValue)
Dim b = pt = New Point(Integer.MinValue, Integer.MinValue)
But go on stretching data
Theoretically, I could overload the +
operator as follows:
Public Shared Operator +(ByVal v As Vector, ByVal amount As Integer) As Vector
Return New Vector(v.X + amount, v.Y + amount)
End Operator
to puzzle the audience with expressions like:
Dim v = Vector.From(4, 5) + 9
But that's too inconsiderate for even me. ;) Instead, see my Add()
overloads, the second one does data stretching:
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
This performs Offset, Inflate/Deflate, and is compatible to Point
and Size
, because of some widening CType()
operators. (.Subtract()
works in the same manner.)
Datastretching with Rect
The benefits of .From()
and .EachEquals()
, of course, work as well, and in the case of .From()
, I support one more stretch option:
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
Also .Add()
becomes more interesting:
Public Function Add(ByVal toEach As Integer) As Rect
Return Add(Vector.From(toEach))
End Function
Public Function Add(ByVal toEach As Vector) As Rect
Return New Rect(Near + toEach, Far + toEach)
End Function
Public Function Add(ByVal v1 As Vector, ByVal v2 As Vector) As Rect
Return New Rect(Near + v1, Far + v2)
End Function
Public Function Add(ByVal r As Rect) As Rect
Return New Rect(Near + r.Near, Far + r.Far)
End Function
(.Subtract()
ditto.)
The above works behind the scenes, when VirtualForm gets the four-edges-offset within one line of code:
_GrabOffs = Rect.From(Control.MousePosition).Subtract(_FormRect)
or when I inflate _FormRect
with snapSize
:
Dim inflated = _FormRect.Add(-snapSize, snapSize)
LINQ
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
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
I don't know whether that is useful. I, for instance, don't use it. But it looks interesting to me, and it was easy to implement. ;)
Replacing public shared functions with extension functions
It's a sort of off-topic, but I'd like to mention one more principle, to make you understand all the shown code:
When it comes to extension functions, in many cases, there is no more need for Public Shared
functions, which forces you to type full qualifiers to call them. Compare these clip-variants:
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
In the second variant, Min()/Max()
appears as an extension function, which is shorter, easier to type (help from intellisense), and which resolves the nasty nesting of functions to an easier to handle concatenation. The same concept I applied to Math.Abs()
(maybe you have realized that already, seeing my previous code-samples).
Even tests to Nothing
, I'd prefer as an extension:
<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
More ideas - the third dimension
I don't really know much about DirectX, but I wonder how DirectX developers manage not to lose their minds. Aren't they forced to type each bit of code thrice? Or, worse, 2.Pow(2)
times? I don't know the exact increase in complexity depending on the dimensions, but a square on a chess-board has 8 neighbors, while a packed cube touches 26 others!
I wonder what a "StickyCubes" application looks like? ;)