Hexagonal Grid Patterns
Overview
This is a GDI+ patterns program that I originally wrote in VB2008, that I’ve rewritten here for C# 2013 Desktop. There are three types of pattern used in this example.
The first (Figure 1) is a simple resizable plain triangle pattern.
The second pattern (Figure 2) is more complicated, as it’s a number pattern with interactive Mousemove features. Depending on the location of the Mousepointer, different rows of numbers are highlighted. This pattern is also resizable.
Figure 3. Sierpinski’s Triangle
The third type of pattern (Figure 3) is Sierpinski’s Triangle.
In a more in depth depiction, the pattern is infinitely repeating as you scroll in, but in this implementation there is no zoom feature.
The patterns are drawn with GDI+ methods, on a PictureBox divided into hexagonal cells. On Mousemove in the PictureBox (in all three types of pattern), the column and row location of the Mousepointer is displayed in a Label below the PictureBox. This could be achieved by a clever calculation, but a simpler GraphicsPath method is used.
When viewing Pascal’s Triangle, additional information is displayed in the Label when moving the mouse over the numbers.
Code – The cell Class
There are two Form level variables used primarily in the PictureBox_Mousemove event, but also used in the various methods called by the Form, PictureBox, ComboBox, and NumericUpDown events.
List<cell> cells = new List<cell>();
Point lastCell = default(Point);
The List cells is of type cell. This is the cell Class.
using System;
using System.Drawing;
namespace Patterns
{
class cell
{
public int column;
public int row;
public Color fillColor;
public string text;
public Color textColor;
}
}
Code – The Form
In Form_Load, the ComboBox is loaded.
private void Form1_Load(object sender, EventArgs e)
{
ComboBox1.DataSource = new string[] {
"Filled Cells Triangle",
"Pascal's Triangle",
"Sierpinski's Triangle"};
}
The other Form event that is handled is the Paint event, where a square grid is drawn over the surface of the Form.
private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
for (int c = 0; c <= this.Width; c += 25) {
e.Graphics.DrawLine(Pens.LightGray, c, 0, c, this.Height);
}
for (int r = 0; r <= this.Height; r += 25) {
e.Graphics.DrawLine(Pens.LightGray, 0, r, this.Width, r);
}
}
In the PictureBox_Paint event, a hexagonal grid is drawn on the PictureBox surface, before any patterns are drawn.
The Form level List cells contains information about which cells to fill either with a color or a number, depending on the type of pattern being drawn. The second loop in the PictureBox_Paint event fills those cells.
private void PictureBox1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
Point[] points = {
new Point(12, 0),
new Point(25, 5),
new Point(25, 18),
new Point(12, 23),
new Point(0, 18),
new Point(0, 5)
};
for (int x = 0; x <= PictureBox1.Width; x += 25)
{
for (int y = 0; y <= PictureBox1.Height; y += 36)
{
e.Graphics.DrawPolygon(Pens.LightGray, Array.ConvertAll(points, p => new Point(p.X + x, p.Y + y)));
e.Graphics.DrawPolygon(Pens.LightGray, Array.ConvertAll(points, p => new Point(p.X + x - 13, p.Y + y + 18)));
}
}
for (int x = 0; x <= cells.Count - 1; x++)
{
Rectangle r = cellRect(cells[x].column, cells[x].row);
if (!(cells[x].fillColor == null))
{
r.Inflate(-6, 0);
e.Graphics.FillEllipse(new SolidBrush(cells[x].fillColor), r);
}
if (!string.IsNullOrEmpty(cells[x].text))
{
float width = e.Graphics.MeasureString(cells[x].text, PictureBox1.Font).Width;
e.Graphics.DrawString(cells[x].text, PictureBox1.Font, new SolidBrush(cells[x].textColor == Color.Empty ? Color.Black: cells[x].textColor), (r.Left + (r.Width - width) / 2) - 1, r.Top);
}
}
}
This is the cellRect function called in the PictureBox_Paint event. It calls two additional functions, cellX and cellTop.
private Rectangle cellRect(int columnIndex, int rowIndex)
{
return new Rectangle(cellX(columnIndex, rowIndex), cellTop(columnIndex, rowIndex) + 5, 25, 13);
}
private int cellX(int columnIndex, int rowIndex)
{
if (rowIndex % 2 == 0)
{
return columnIndex * 25;
}
else
{
return (columnIndex * 25) - 12;
}
}
private int cellTop(int columnIndex, int rowIndex)
{
if (rowIndex % 2 == 0)
{
if (rowIndex == 0)
{
return 0;
}
return Convert.ToInt32(rowIndex / 2) * 36;
}
else
{
return Convert.ToInt32(Math.Ceiling(Convert.ToDecimal(rowIndex) / 2) * 36 - 18);
}
}
By far the most complicated Control event in this example is the PictureBox_MouseMove event. The code first checks if the Mousepointer has moved to a different cell. If it has, the cell location is calculated and displayed in the Label below the PictureBox, then any highlighting is achieved by changing the Properties in the cells List and finalized by repainting the picturebox.
private void PictureBox1_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
{
Point p = cellFromPoint(e.Location);
if (p != lastCell)
{
lastCell = p;
}
else
{
return;
}
Label1.Text = string.Format("Column = {0}, Row = {1}", p.X, p.Y);
if (ComboBox1.SelectedIndex == 1)
{
cells = Array.ConvertAll(cells.ToArray(), (cell c) => new cell
{
textColor = Color.Black,
column = c.column,
row = c.row,
fillColor = c.fillColor,
text = c.text
}).ToList();
PictureBox1.Invalidate();
int isUsed = Array.FindIndex(cells.ToArray(), (cell c) => c.column == p.X & c.row == p.Y);
if (isUsed != -1)
{
cells = Array.ConvertAll(cells.ToArray(), (cell c) => new cell
{
textColor = c.row == p.Y ? Color.Red : Color.Black,
column = c.column,
row = c.row,
fillColor = c.fillColor,
text = c.text
}).ToList();
PictureBox1.Invalidate();
int sum = cells.Where((cell c) => c.row == p.Y).Sum((cell c) => Convert.ToInt32(c.text));
int power = 0;
while (Math.Pow(2, power) != sum)
{
power += 1;
}
Label1.Text = string.Format("Column = {0}, Row = {1}{2}Row sum = {3} (2^{4})", p.X, p.Y, Environment.NewLine, sum, power);
}
else
{
if (p.X < 6)
{
int startColumn = 6;
int startRow = 3;
cell c1 = Array.Find(cells.ToArray(), (cell c) => c.column == startColumn & c.row == startRow);
while ((c1 != null))
{
c1.textColor = Color.Red;
startRow += 1;
c1 = Array.Find(cells.ToArray(), (cell c) => c.column == startColumn & c.row == startRow);
if (c1 == null)
break;
c1.textColor = Color.Red;
startColumn += 1;
startRow += 1;
c1 = Array.Find(cells.ToArray(), (cell c) => c.column == startColumn & c.row == startRow);
}
}
else if (p.X > 6)
{
int startColumn = 7;
int startRow = 3;
cell c1 = Array.Find(cells.ToArray(), (cell c) => c.column == startColumn & c.row == startRow);
startColumn -= 1;
while ((c1 != null))
{
c1.textColor = Color.Red;
startRow += 1;
c1 = Array.Find(cells.ToArray(), (cell c) => c.column == startColumn & c.row == startRow);
if (c1 == null)
break;
c1.textColor = Color.Red;
startRow += 1;
c1 = Array.Find(cells.ToArray(), (cell c) => c.column == startColumn & c.row == startRow);
startColumn -= 1;
}
}
}
}
}
This is the cellFromPoint function called in the PictureBox_MouseMove event. It calls the PointIsInPolygon function when locating the cell.
private Point cellFromPoint(Point p)
{
Point[] points = {
new Point(12, 0),
new Point(25, 5),
new Point(25, 18),
new Point(12, 23),
new Point(0, 18),
new Point(0, 5)
};
int y = 0;
int columnIndex = 0;
for (y = -1; y <= 20; y++)
{
int offset = y % 2 == 0 ? 0 : -13;
List<Point[]> polygons = new List<Point[]>(Enumerable.Range(0, 20).Select((int x) => Array.ConvertAll<Point, Point>(points, (Point p1) => new Point((p1.X + offset + (x * 25)), p1.Y + (y * 18)))));
columnIndex = Array.FindIndex(polygons.ToArray(), (Point[] p1) => PointIsInPolygon(p1, p));
if (columnIndex != -1)
break;
}
return new Point(columnIndex, y);
}
// Return true if the point is inside the polygon.
private bool PointIsInPolygon(Point[] polygon, Point target_point)
{
// Make a GraphicsPath containing the polygon.
GraphicsPath path = new GraphicsPath();
path.AddPolygon(polygon);
// See if the point is inside the path.
return path.IsVisible(target_point);
}
When the ComboBox SelectedIndex is changed the Form is resized and the displayTriangle method is called to change the type of pattern. Two of the pattern types are resizable.
The displayTriangle method calls either the fillCell method, and depending on the pattern type, the drawSubTriangle method, or the setText method. These subsidiary methods just manipulate the cells List and all of the drawing is still done from the values in that List, in the PictureBox_Paint event.
private void ComboBox1_SelectedIndexChanged(System.Object sender, System.EventArgs e)
{
switch (ComboBox1.SelectedIndex)
{
case 2:
this.Size = new Size(515, 555);
NumericUpDown1.Enabled = false;
break;
default:
this.Size = new Size(366, 445);
NumericUpDown1.Enabled = true;
break;
}
displayTriangle();
}
private void displayTriangle()
{
cells.Clear();
int startColumn = 0;
switch (ComboBox1.SelectedIndex)
{
case 0:
startColumn = 6;
fillCell(Color.Red, startColumn, 2);
for (int y = 3; y <= 3 + (Convert.ToInt32(NumericUpDown1.Value) - 2); y++)
{
for (int x = 0; x <= y - 2; x++)
{
fillCell(Color.Red, startColumn + x, y);
}
if (y % 2 == 1)
{
startColumn -= 1;
}
}
break;
case 1:
List<int[]> triangle = new List<int[]>();
triangle.Add(new int[] { 1 });
triangle.Add(new int[] {1,1});
for (int y = 3; y <= Convert.ToInt32(NumericUpDown1.Value); y++)
{
List<int> line = new List<int>(new int[] { 1 });
for (int x = 0; x <= triangle.Last().GetUpperBound(0); x++)
{
if (x < triangle.Last().GetUpperBound(0))
{
line.Add(triangle.Last().ToArray()[x] + triangle.Last().ToArray()[x + 1]);
}
}
line.Add(1);
triangle.Add(line.ToArray());
}
startColumn = 6;
setText(triangle[0][0].ToString(), startColumn, 2);
for (int y = 3; y <= 3 + (Convert.ToInt32(NumericUpDown1.Value) - 2); y++)
{
for (int x = 0; x <= y - 2; x++)
{
setText(triangle[y - 2][x].ToString(), startColumn + x, y);
}
if (y % 2 == 1)
startColumn -= 1;
}
break;
case 2:
startColumn = 9;
fillCell(Color.Red, startColumn, 2);
for (int y = 3; y <= 3 + 14; y++)
{
for (int x = 0; x <= y - 2; x++)
{
fillCell(Color.Red, startColumn + x, y);
}
if (y % 2 == 1)
startColumn -= 1;
}
startColumn = 9;
fillCell(Color.Black, startColumn, 16);
for (int y = 15; y >= 10; y += -1)
{
for (int x = 0; x <= 16 - y; x++)
{
fillCell(Color.Black, startColumn + x, y);
}
if (y % 2 == 1)
startColumn -= 1;
}
drawSubTriangle(9, 4, 8);
drawSubTriangle(5, 12, 16);
drawSubTriangle(13, 12, 16);
break;
}
PictureBox1.Invalidate();
}
private void fillCell(Color fillColor, int columnIndex, int rowIndex)
{
cells.Add(new cell
{
column = columnIndex,
row = rowIndex,
fillColor = fillColor
});
PictureBox1.Invalidate();
}
private void setText(string text, int columnIndex, int rowIndex)
{
cells.Add(new cell
{
column = columnIndex,
row = rowIndex,
text = text
});
PictureBox1.Invalidate();
}
private void drawSubTriangle(int startColumn, int startRow, int endRow)
{
int sC = startColumn;
fillCell(Color.Black, sC, startRow);
for (int y = endRow; y >= startRow + 2; y += -1)
{
for (int x = 0; x <= endRow - y; x++)
{
fillCell(Color.Black, sC + x, y);
}
if (y % 2 == 1)
{
sC -= 1;
}
}
fillCell(Color.Black, startColumn - 2, endRow);
fillCell(Color.Black, startColumn + 2, endRow);
}
The resizable patterns are resized by means of a NumericUpDown Control.
private void NumericUpDown1_ValueChanged(System.Object sender, System.EventArgs e)
{
displayTriangle();
}