Introduction
It’s all rectangles. Draw a rectangle and you need a rectangle control. Draw an ellipse and you need the bounding rectangle. Drawing controls, graphics, positioning forms—these all require rectangles.
My upcoming book, LINQ Unleashed for C# from Sams, talks about Language INtegrated Query. LINQ produces sequences. I wanted to visualize those sequences for the books—I got tired of creating them by hand in MS-Paint—so I thought I’d write a utility to generate the LINQ visualizations for me. Guess what? Rectangles are needed to lay out each item in the sequence.
In this article, a general utility for laying out rectangles and sub-rectangles in a line is provided. The utility also will lay out multiple rows of rectangles and sub-rectangles. Finally, the utility is used to display the results of LINQ queries. Enjoy.
Defining the Rectangle Utility
The basic objective is to define a simple utility that will lay out rectangles and sub-rectangles. The utility includes the ability to center a rectangle; this can be used to lay out text within the rectangle.
The solution chosen is a shared class that defines a couple of functions to return a rectangle and some number and position of smaller rectangles within the outer rectangle. The solution supports inserting padding between rectangles for a uniform, evenly spaced layout. Finally, some of the methods are overloaded to support rectangles with integer coordinates and those with floating point coordinates. Listing 1 contains the solution.
Listing 1: A general rectangle utility.
Imports System.Drawing Public Class Rectangles ' example: a rectangle divided four across and three down ' get h=2 and v = 2 the x shows the sub-rect we would get ' ----------------- ' | | | | | ' ----------------- ' | | x | | | ' ----------------- ' | | | | | ' ----------------- ' Given a rectangle return a sub-rectangle based on horizontal ' and vertical sub-divsions Public Shared Function GetSubRect(ByVal x As Integer, _ ByVal y As Integer, _ ByVal width As Integer, ByVal height As Integer, _ ByVal horizontalSegment As Integer, _ ByVal verticalSegment As Integer, _ ByVal numberOfHorizontalSegments As Integer, _ ByVal numberOfVerticalSegments As Integer) As Rectangle Return GetSubRect(x, y, width, height, horizontalSegment, _ verticalSegment, numberOfHorizontalSegments, _ numberOfVerticalSegments, 0) End Function ' Given a rectangle return a sub-rectangle based on horizontal ' and vertical sub-divsions Public Shared Function GetSubRectF(ByVal x As Integer, _ ByVal y As Integer, _ ByVal width As Single, ByVal height As Single, _ ByVal horizontalSegment As Single, _ ByVal verticalSegment As Single, _ ByVal numberOfHorizontalSegments As Integer, _ ByVal numberOfVerticalSegments As Integer) As RectangleF Return GetSubRect(x, y, width, height, horizontalSegment, _ verticalSegment, numberOfHorizontalSegments, _ numberOfVerticalSegments, 0) End Function ' Given a rectangle with padding in between ' return a sub-rectangle based on horizontal and vertical ' sub-divsions Public Shared Function GetSubRect(ByVal x As Integer, _ ByVal y As Integer, _ ByVal width As Integer, ByVal height As Integer, _ ByVal horizontalSegment As Integer, _ ByVal verticalSegment As Integer, _ ByVal numberOfHorizontalSegments As Integer, _ ByVal numberOfVerticalSegments As Integer, _ ByVal padding As Integer) As Rectangle Dim newWidth As Integer = width / _ numberOfHorizontalSegments - padding Dim newHeight As Integer = height / _ numberOfVerticalSegments - padding Return New Rectangle(x + (newWidth + padding) * _ horizontalSegment, _ y + (newHeight + padding) * verticalSegment, _ newWidth, newHeight) End Function ' Given a rectangle with padding in between ' return a sub-rectangle based on horizontal and vertical ' sub-divsions Public Shared Function GetSubRectF(ByVal x As Integer, ByVal y As Integer, _ ByVal width As Single, ByVal height As Single, _ ByVal horizontalSegment As Single, _ ByVal verticalSegment As Single, _ ByVal numberOfHorizontalSegments As Integer, _ ByVal numberOfVerticalSegments As Integer, _ ByVal padding As Single) As RectangleF Dim newWidth As Single = width / _ numberOfHorizontalSegments - padding Dim newHeight As Single = height / _ numberOfVerticalSegments - padding Return New Rectangle(x + (newWidth + padding) * _ horizontalSegment, y + (newHeight + padding) * _ verticalSegment, _ newWidth, newHeight) End Function ' Given a rectangle return a sub-rectangle based on horizontal _ ' and vertical sub-divsions Public Shared Function GetSubRect(ByVal boundingRect _ As Rectangle, _ ByVal horizontalSegment As Integer, _ ByVal verticalSegment As Integer, _ ByVal numberOfHorizontalSegments As Integer, _ ByVal numberOfVerticalSegments As Integer) As Rectangle Return GetSubRect(boundingRect.X, boundingRect.Y, _ boundingRect.Width, _ boundingRect.Height, horizontalSegment, _ verticalSegment, numberOfHorizontalSegments, _ numberOfVerticalSegments) End Function ' Given a rectangle return a sub-rectangle based on horizontal ' and vertical sub-divsions Public Shared Function GetSubRectF(ByVal boundingRect _ As RectangleF, _ ByVal horizontalSegment As Single, _ ByVal verticalSegment As Single, _ ByVal numberOfHorizontalSegments As Integer, _ ByVal numberOfVerticalSegments As Integer) As RectangleF Return GetSubRectF(boundingRect.X, boundingRect.Y, _ boundingRect.Width, _ boundingRect.Height, horizontalSegment, _ verticalSegment, numberOfHorizontalSegments, _ numberOfVerticalSegments) End Function ' Given a rectangle with padding in between ' return a sub-rectangle based on horizontal and vertical ' sub-divsions Public Shared Function GetSubRect(ByVal boundingRect _ As Rectangle, _ ByVal horizontalSegment As Integer, _ ByVal verticalSegment As Integer, _ ByVal numberOfHorizontalSegments As Integer, _ ByVal numberOfVerticalSegments As Integer, _ ByVal padding As Integer) As Rectangle Return GetSubRect(boundingRect.X, boundingRect.Y, _ boundingRect.Width, _ boundingRect.Height, horizontalSegment, _ verticalSegment, numberOfHorizontalSegments, _ numberOfVerticalSegments, padding) End Function ' Given a rectangle with padding in between ' return a sub-rectangle based on horizontal and vertical ' sub-divsions Public Shared Function GetSubRectF(ByVal boundingRect _ As RectangleF, _ ByVal horizontalSegment As Single, _ ByVal verticalSegment As Single, _ ByVal numberOfHorizontalSegments As Integer, _ ByVal numberOfVerticalSegments As Integer, _ ByVal padding As Single) As RectangleF Return GetSubRectF(boundingRect.X, boundingRect.Y, _ boundingRect.Width, _ boundingRect.Height, horizontalSegment, _ verticalSegment, numberOfHorizontalSegments, _ numberOfVerticalSegments, padding) End Function ' Given the size of something get a centered horizontally ' and vertically centered rectangle Public Shared Function CenterRect(ByVal size As SizeF, _ ByVal boundingRectangle As Rectangle) Return New Rectangle(boundingRectangle.X + _ (boundingRectangle.Width - size.Width) / 2, _ boundingRectangle.Y + (boundingRectangle.Height - _ size.Height) / 2, _ boundingRectangle.Width, _ boundingRectangle.Height) End Function End Class
The utility is capable of producing rectangle sub-divisions as suggested by the roughly created grid in the comment in the first part of the listing. You can specify the number of vertical and horizontal sub-rectangles, which horizontal and vertical sub-rectangle to return, and CenterRect (at the end) will return a rectangle centered vertically and horizontally within another rectangle.
The reason shared methods were used is that the Rectangles class itself stores no data; that is, it contains no fields, properties, or events. Following a general OOP rule such a class is naturally specified as a class containing all shared members.
Defining a State-Full Rectangle Manager
Next, I wanted to able to track individual rectangles, so a state-full RectangleManager was defined. This class keeps track of its horizontal and vertical sub-rectangles (with hSegments and vSegments), the desired padding between rectangles, and the original outer rectangle. This class is useful for keeping track of a specific rectangle set being used. Listing 2 contains the RectangleManager.
Listing 2: The RectangleManager is state-full—keeping track of the rectangle under management.
Imports System.Collections Public Class RectangleManager 'Implements IEnumerator ' note to self - implement IEnumerator so we can do a foreach ' over the segments Public Sub New(ByVal Rect As Rectangle, _ ByVal hSegments As Integer, ByVal vSegments As Integer) Me.Rect = Rect Me.vSegments = vSegments Me.hSegments = hSegments End Sub Public Sub New(ByVal Rect As Rectangle, _ ByVal hSegments As Integer, _ ByVal vSegments As Integer, ByVal padding As Integer) Me.Rect = Rect Me.vSegments = vSegments Me.hSegments = hSegments Me.padding = padding End Sub Private Rect As Rectangle Private vSegments As Integer Private hSegments As Integer Private padding As Integer = 4 Public Function GetSubRect(ByVal xSegment As Integer, _ ByVal ySegment As Integer) As Rectangle Return Rectangles.GetSubRect(Rect, xSegment, ySegment, _ hSegments, _ vSegments, padding) End Function Public Function CenterRect(ByVal size As SizeF, _ ByVal xSegment As Integer, _ ByVal ySegment As Integer) Return Rectangles.CenterRect(size, GetSubRect(xSegment, _ ySegment)) End Function End Class
Visualizing a LINQ Query with the Rectangle Manager
A LINQ query returns a sequence. (Think collection!) By using ellipses, you can visualize the sequence as a linear row of circles with each circle containing the text (or a representation of the text) in the circle itself. Listing 3 shows the form for representing the sequence visually, and Listing 4 contains a concrete behavior class that contains the draw-sequence behavior. (You can look up the State Behavior Pattern on dofactory.com or the GoF book on Design Patterns.)
Listing 3: The Form using a state behavior pattern; the Form shows the visual representation of the LINQ query.
Public Class Form1 Private state As MyStatePattern Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load state = New DrawSequenceState(p) End Sub Private p As Pen = New Pen(Color.Red, 4) Private Sub Form1_Paint(ByVal sender As Object, _ ByVal e As System.Windows.Forms.PaintEventArgs) _ Handles Me.Paint state.Draw(e.Graphics, Font) End Sub Private Sub DrawRectanglesToolStripMenuItem_Click(ByVal _ sender As System.Object, _ ByVal e As System.EventArgs) Handles _ DrawRectanglesToolStripMenuItem.Click state = New DrawRectState(p) Invalidate() End Sub Private Sub DrawEllipsesToolStripMenuItem_Click(ByVal sender _ As System.Object, _ ByVal e As System.EventArgs) Handles _ DrawEllipsesToolStripMenuItem.Click state = New DrawEllipseState(p) Invalidate() End Sub Private Sub DrawSequenceToolStripMenuItem_Click(ByVal sender _ As System.Object, _ ByVal e As System.EventArgs) Handles _ DrawSequenceToolStripMenuItem.Click state = New DrawSequenceState(p) Invalidate() End Sub End Class
Listing 4: The DrawSequenceState class that contains the sequence and the call using the RectangleManager to represent that sequence.
Imports System.Drawing Public Class DrawSequenceState Inherits MyStatePattern ''' <summary> ''' Initializes a new instance of the DrawSequenceState class. ''' </summary> Public Sub New(ByVal pen As Pen) MyBase.New(pen) End Sub Public Overrides Sub Draw(ByVal Graphics As _ System.Drawing.Graphics, _ ByVal Font As System.Drawing.Font) Dim i = New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9} Graphics.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias Dim r As Rectangle = New Rectangle(10, 50, 200, 50) Graphics.DrawRectangle(MyPen, r) Dim result = From o In i Where o Mod 2 = 0 Select o Dim instance As RectangleManager = _ New RectangleManager(r, result.Count, 1) For x = 0 To result.Count - 1 Dim f As Rectangle = instance.GetSubRect(x, 0) Graphics.FillEllipse(Brushes.Lavender, f.X, f.Y, _ f.Width, f.Height) Graphics.DrawEllipse(Pens.Black, f.X, f.Y, _ f.Width, f.Height) Dim t As Rectangle = instance.GetSubRect(x, 0) Dim s As SizeF = Graphics.MeasureString( _ result.ElementAt(x).ToString(), Font) Dim textRect As Rectangle = Rectangles.CenterRect(s, t) Graphics.DrawString(result.ElementAt(x).ToString(), _ Font, _ Brushes.Blue, textRect.X, textRect.Y) Next End Sub End Class
The LINQ query is shown in Listing 4 on the line Dim result = From o In i Where o Mod 2 = 0 Select 0. This query selects the even numbered integers as shown in Figure 1.
Figure 1: The results of the LINQ query returning even numbers in a set of integers 0 through 8.
The base class for the state pattern MyStatePattern is shown in Listing 5.
Listing 5: The abstract base class for the state pattern.
Public MustInherit Class MyStatePattern Private FMyPen As Pen Public Property MyPen() As Pen Get Return FMyPen End Get Set(ByVal Value As Pen) FMyPen = Value End Set End Property Public Sub New(ByVal pen As Pen) FMyPen = pen End Sub Public MustOverride Sub Draw(ByVal Graphics As Graphics, _ ByVal Font As Font) End Class
Summary
Everything is Rectangles in a 2D rectilinear world. In this article, I demonstrated how you can draw specific rectangular sub-regions as rectangles or ellipses (or really as anything that is constrained by a rectangular region). That was followed by an application of this technique that visualizes LINQ queries. I hope you find the code useful.
About the Author
Paul Kimmel is the VB Today columnist for www.codeguru.com and has written several books on object-oriented programming and .NET. Check out his upcoming book LINQ Unleashed for C# due in Spring 2008. You may contact him for technology questions at pkimmel@softconcepts.com.
If you are interested in joining or sponsoring a .NET Users Group, check out www.glugnet.org. Glugnet opened a users group branch in Flint, Michigan in August 2007. If you are interested in attending, check out the www.glugnet.org web site for updates.
Copyright © 2008 by Paul T. Kimmel. All Rights Reserved.