[C# Helper]
Index Books FAQ Contact About Rod
[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

[C# 24-Hour Trainer]

[Beginning Software Engineering]

[C# 5.0 Programmer's Reference]

[MCSD Certification Toolkit (Exam 70-483): Programming in C#]

Title: Print circles centered inside other closely packed circles in C#

[Print circles centered inside other closely packed circles in C#]

This example shows how to print circles that are arranged densely in a hexagonal pattern. I needed that to make a cardboard sheet for arranging cupcakes to make a cupcake cake. The holes needed to be 1" in radius and spaced to allowed the 1.5" radius cupcake tops to pack tightly. You may never need to do this (in fact, we didn't end up using it), but you may find the program interesting.

I spent quite a while googling around looking for some sort of tem plate that I could use. Then I realized that this is actually pretty easy. In the end I wrote the program in less time than I wasted in fruitless googling.

A Little Math

[Print circles centered inside other closely packed circles in C#]

The key to arranging the circles is the little bit of high school geometry shown in the picture on the right. Consider the larger circles drawn with dashed lines in the picture at the top of the post. If the circles are packed tightly, then the dashed triangle shown on the right is a 30-60-90 triangle. In that case, it has relative side lengths 1, 2, √3. The shortest side length in the figure has length R where R is the radius of the outer (dashed) circles. That means the triangle's other sides have lengths R * 2 and R * √3.

That means to get from the upper left circle to the circle to the right on the next lower row, you increase the circle's X coordinate by R and its Y coordinate by R * √3.

That's all the math you need. The following sections explain how the program's code uses those values to arrange the circles.

Launching the Print Out

When you enter the radii and click Preview, the following code executes.

private float InnerRadius, OuterRadius; // Display the PrintPreviewDialog. private void btnPreview_Click(object sender, EventArgs e) { InnerRadius = 100 * float.Parse(txtInnerRadius.Text); OuterRadius = 100 * float.Parse(txtOuterRadius.Text); Form frm = ppdCircles as Form; frm.WindowState = FormWindowState.Maximized; ppdCircles.PrintPreviewControl.Zoom = 1.0; ppdCircles.PrintPreviewControl.UseAntiAlias = true; ppdCircles.ShowDialog(); }

The variables InnerRadius and OuterRadius hold the radii of the inner (solid) and outer (dashed) circles.

The btnPreview_Click event handler gets the values that you entered in the text boxes, multiplies them by 100, and saves them in those variables. It multiples them by 100 because the printer's units are 100ths of inches. For example, a radius of 1.00 inches corresponds to 100 printer units.

Next, the code uses the ppdCircles component. That component is a PrintPreviewDialog that I added to the program's form at design time. The code creates a variable of type Form and casts the dialog into it. That is possible because a PrintPreviewDialog is a kind of form.

Now the code can use the dialog's form properties, which are hidden when you work with the dialog as a PrintPreviewDialog. The program uses the Form variable to set the dialog's WindowState property to Maximized so the dialog begins maximized.

Next, the code sets the dialog's zoom level to 1.0 so the printout will be displayed at full scale. (In the picture at the top of the post, I reduced the zoom to 50% so you could see more circles.) The code also makes the dialog use anti-aliasing so the print preview is smooth.

Finally, the code displays the dialog.


When the PrintPreviewDialog is displayed, it uses a PrintDocument object to generate the image that it will display. At design time, I added a PrintDocument object named pdocCircles to the form. I also set the PrintDocument object's PrintDocument property to ppdocCircles so the dialog could find that object when needed.

When the dialog is displayed, it invokes the PrintDocument object's PrintPage event handler. The following code shows the event handler used by this example to print circles.

// Generate the printout. private void pdocCircles_PrintPage(object sender, System.Drawing.Printing.PrintPageEventArgs e) { float cx = e.MarginBounds.Left + OuterRadius; float cy = e.MarginBounds.Top + OuterRadius; float dx = OuterRadius; float dy = (float)(OuterRadius * Math.Sqrt(3)); using (Pen dashed_pen = new Pen(Color.Black)) { dashed_pen.DashPattern = new float[] { 10, 10 }; while (cy - OuterRadius < e.MarginBounds.Bottom) { // Make rectangles for the first circle. RectangleF outer_rect = new RectangleF( cx - OuterRadius, cy - OuterRadius, 2 * OuterRadius, 2 * OuterRadius); RectangleF inner_rect = new RectangleF( cx - InnerRadius, cy - InnerRadius, 2 * InnerRadius, 2 * InnerRadius); // Draw a row. while (outer_rect.Left < e.MarginBounds.Right) { e.Graphics.DrawEllipse(dashed_pen, outer_rect); e.Graphics.DrawEllipse(Pens.Black, inner_rect); outer_rect.X += 2 * OuterRadius; inner_rect.X += 2 * OuterRadius; } // Prepare for the next row. cx -= dx; if (cx < e.MarginBounds.Left) cx += 2 * OuterRadius; cy += dy; } } }

This code sets variables cx and cy to the coordinates for the center of the first circle. It starts at the upper left corner of the page inside its margins. It then adds the radius of the outer circle to that point's X and Y coordinates. The result will place the first circle so it just touches the upper left corner of the page margins.

Next, the code sets variables dx and dy to the distances that it must move a circle's center to get to a circle in the next lower row. (See the earlier section "A Little Math.")

The code then creates a dashed pen and sets its DashPattern to produce a nice dashed line. It then enters a while loop that lasts as long as the value cy is small enough that subtracting the outer circle's radius keeps part of the circle within the page's bounds. (Alternatively you could change this so it only draws complete circles, but I decided to print any circle that lies at least partly inside the page's margins.)

Now the code makes two rectangles to draw the inner and outer circles centered at the point (cx, cy) and with the desired radii.

The program then enters a loop to draw the row. The loop lasts as long as a circle centered at (cx, cy) will fit at least partly within the page margins horizontally. Within the loop, the program draws the inner and outer circles and then adds 2 * OuterRadius to the rectangles' X coordinates to move to the next circle in the row.

After it has finished drawing a row, the code subtracts dx from cx. Then if cx is less than the page's left margin, the code adds 2 * OuterRadius to it so the first circle on the next row fits within the page margins.

Finally, the code adds dy to cy to move down to the next row.

When the PrintPage event handler ends, it can indicate whether there are more pages to print by setting e.HasMorePages to true or false. This example prints only one page, so it should set e.HasMorePages to false. That is the initial value when the event handler executes, so the code doesn't need to change it.


That's all you need to do to print circles that are packed tightly in a hexagonal pattern. You can print directly by calling the PrintDocument object's Print method if you like. I usually just display a preview. That way I can test the program as many times as necessary without wasting paper. When everything looks correct, I press the preview dialog's Print button to actually print.

It turned out that natural variation in the side of the cupcakes made them not fit tightly together, so we threw away the cardboard pattern that I had painstakingly made and we simply glued each cupcake down on a pan with a little bit of frosting. That was much easier and allowed more flexibility in positioning. Just use this post as an example showing how arrange circles, print circles, and use the PrintDocument and PrintPreviewDialog components to create a printout in general.

Download the example to experiment with it and to see additional details.

© 2009-2022 Rocky Mountain Computer Consulting, Inc. All rights reserved.