The BMPStitch utility contains a few code fragments worthy of discussion and study, especially for someone uninitiated with the .NET Framework. Some of the functionality may serve as a learning tool into this vast library.
The most interesting part of the BMPStitch code is a class I developed for displaying the image and the clipping lines. This class is ClipPictureBox, and Listing C-1 shows the interface of the class.
Listing C.1: Public (and Protected) Interface of the ClipPictureBox Class
Public Class ClipPictureBox Inherits PictureBox Property ClipColor() As Color Property ClipDisabled() As Boolean Public Event ClippingRectChanged(ByVal r As Rectangle) ReadOnly Property ClippingRect() As Rectangle Shadows Property Image() As Image Protected Overrides Sub OnPaint(ByVal pe As _ System.Windows.Forms.PaintEventArgs) Protected Overrides Sub OnMouseDown(ByVal e As _ System.Windows.Forms.MouseEventArgs) Protected Overrides Sub OnMouseMove(ByVal e As _ System.Windows.Forms.MouseEventArgs) Protected Overrides Sub OnMouseUp(ByVal e As _ System.Windows.Forms.MouseEventArgs) End Class
The ClipPictureBox class is an ancestor of the standard .NET Framework PictureBox class, meaning of course that it receives all of that ancestor’s functionality “for free.” This class extends the ancestor class by supporting the drawing and manipulation of the clipping lines. To that end, the OnPaint method and the three mouse-based methods OnMouseDown, OnMouseMove, and OnMouseUp are all overridden.
Another case of extended functionality occurs in the Image property, which is aproperty found in the base class. You can extend the functionality of the property so that you can initialize the location of the four clipping lines. You do this by using the Shadows keyword on the property, which indicates that this property replaces the functionality found in the base class. Listing C-2 shows the code for the Image property, as well as the private variables used to store the location of the four clipping lines.
Listing C.2: Shadowed Image Property on the ClipPictureBox
Private FClipInitialized As Boolean = False Private FClipTop As Integer Private FClipBottom As Integer Private FClipLeft As Integer Private FClipRight As Integer Shadows Property Image() As Image Get Return MyBase.Image End Get Set(ByVal Value As Image) MyBase.Image = Value If Not FClipInitialized Then FClipTop = Height \ 4 FClipBottom = (Height \ 4) * 3 FClipLeft = Width \ 4 FClipRight = (Width \ 4) * 3 FClipInitialized = True End If End Set End Property
What’s interesting here is that you’ve specified to replace the Image property by using the Shadows keyword, but then you go ahead and use the base Image property to store the image. In effect, you’re declaring that you want to replace the base property, but then you go ahead and use the property for storage anyway. You then add some functionality to the Set portion of the property. This new functionality initialized the four clipping line variables (only if they haven’t already been set; the variable FClipInitialized acts as a flag to make sure the initialization happens only once).
This technique of shadowing a property and then using it anyway is a great way to “cheat” on a member that isn’t declared Overrideable. The standard way of replacing or extended functionality on a class member is to override it, but you can’t do this unless the base class explicitly declares the class Overrideable. You can shadow any member, though.
The code within the utility’s form has some base file handling functionality that’s important. The first example of this functionality, shown in Listing C-3, occurs when the user clicks the Open button.
Listing C.3: Getting a File from the User, and Setting Up the ClipPictureBox Control
Public Class fBMPStitch Inherits System.Windows.Forms.Form Private FBaseFile As String Dim FFrames As Integer Dim pb As ClipPictureBox Private Sub cbFile_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cbFile.Click If oDialog.ShowDialog Then FBaseFile = oDialog.FileName If pb Is Nothing Then pb = New ClipPictureBox pb.SizeMode = PictureBoxSizeMode.AutoSize pb.Location = New Point(10, 10) pb.Visible = True AddHandler pb.ClippingRectChanged, _ AddressOf pb_ClippingRectChanged Me.Controls.Add(pb) End If pb.Image = Image.FromFile(FBaseFile) Call CountFiles() End If End Sub Private Sub CountFiles() Dim f As New FileInfo(FBaseFile) Dim d As New DirectoryInfo(f.DirectoryName) Dim cExt As String = f.Extension FFrames = 0 For Each f In d.GetFiles("*" & cExt) FFrames += 1 Next lbFrames.Text = FFrames & " frames" nudAcross.Value = Math.Sqrt(FFrames) nudDown.Value = nudAcross.Value End Sub
The variable oDialog is an instance of the .NET Framework class named OpenFileDialog. As shown in Listing C-3, the method ShowDialog opens the dialog box that requests a file from the user and returns True if the user does indeed select a file (the method returns False if the end user selects Cancel).
If the user selects a file, the code initializes a ClipPictureBox variable and adds it to the form. You could’ve done this by putting the ClipPictureBox into its own project and adding it to the Visual Studio toolbox, but this method does it at runtime instead. Once the ClipPictureBox is added to the form, it’s loaded with the image from the selected file.
The routine CountFiles is called next. This routine uses some .NET Framework file and directory handling classes to count the number of files in the folder of the user-selected file. The first class, named FileInfo, encapsulates the functionality of any on-disk file. In this code, the class is initialized with the file selected by the user. The next line initializes a class named DirectoryInfo using the directory of the user-selected file as the constructor’s parameter. What this gives you is a representation of the folder in which the user-selected file lies. One of the things the DirectoryInfo class lets you do is loop through all the files in the represented directory, and this is exactly what the remainder of the code does. The purpose of this loop is to count the number of files in the folder that have the same extension as the originally selected file. Once the number of files is known, it’s stored in the variable FFrames, and the across/down controls are initialized to numbers that are the square root of this value. This works great when there are 36 frames because there would be six frames across and six down, but it doesn’t quite work with, say, 24 frames. It does provide a reasonable starting value for the across/down controls, though.
Listing C-4 shows the routine that takes the individual bitmap files, smashes them into a large single bitmap, and then saves the bitmap to disk.
Listing C.4: The “Meat” of the Bitmap Stitcher: The Stitching Code
Private Sub cbBuild_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cbBuild.Click Dim b As Bitmap Dim f As New FileInfo(FBaseFile) Dim d As New DirectoryInfo(f.DirectoryName) Dim x, y As Integer Dim h, w As Integer Dim iA, iD As Integer Dim cExt As String iA = nudAcross.Value iD = nudDown.Value If iA * iD <> FFrames Then MsgBox("across * down must = frames", _ MsgBoxStyle.Critical + MsgBoxStyle.OKOnly, "Error") Else w = pb.ClippingRect.Width h = pb.ClippingRect.Height b = New Bitmap(w * iA, h * iD, Graphics.FromImage(pb.Image)) Dim g As Graphics g = Graphics.FromImage(b) Try x = 0 y = 0 cExt = f.Extension For Each f In d.GetFiles("*" & cExt) pb.Image = Image.FromFile(f.FullName) pb.Refresh() Thread.CurrentThread.Sleep(100) g.DrawImageUnscaled(pb.Image, x, y) x += w If x >= w * iA Then x = 0 y += h End If Next Finally g.Dispose() End Try b.Save("c:\BMPStitch.bmp", System.Drawing.Imaging.ImageFormat.Bmp) lbClip.Text = "c:\BMPStitch.bmp saved" End If End Sub
The first thing this routine does is check that the across/down control values, when multiplied together, actually add up to the number of frames found in the CountFrames routine discussed previously. If not, then the routine displays an error to the end user and exits.
If the math all works out, though, the bitmap stitching can begin. First, abitmap simply named b is created that has the width and height needed to store the frame bitmaps. The program then loops through all the image files in the folder again, just as in the routine CountFrames. This time, though, each file is loaded into the ClipPictureBox variable and then copied into the appropriate spot on the final bitmap. Finally, the bitmap is saved to the hard-coded filename c:\BMPStitch.bmp.