Introduction
This is a very simple C# graphics program that displays the famous Mandelbrot set, lets you zoom in with a simple selection rectangle, and shows how to smooth the colours, and not flicker.
Background
Most people are familiar with the Mandelbrot Set, a easy to compute, but very pretty fractal. Typical source code looks like this. The linked code works fine, but for the C# programmer you will generally run into two problems: banded colours, and horrible flickering when you implement a selection rectangle.
I'm assuming you can create a project, drag tools onto a form, and program simple event handlers.
Using the Code
This is C# code framework 4.0 using Visual Studio 10.
My program is a simple form with a panel to draw the Mandelbrot set on, a draw button to re-draw the current image, and a reset button to start over and unzoom.
We want to set a few variables when our program starts:
public FormMandelbrot()
{
InitializeComponent();
cx0 = -2.0; cx1 = 0.5;
cy0 = -1.0;
cy1 = 1.0;
Map = new Bitmap(panelMain.Width, panelMain.Height);
}
Draw button event code looks like:
private void buttonDraw_Click(object sender, EventArgs e)
{
doDrawMandelbrot = true;
panelMain.Refresh();
}
You never actually do any drawing in event handlers. You just let the program know what it needs to know to draw, and call refresh to do trigger actually drawing something in the Paint
event.
Reset button event code looks like:
private void buttonReset_Click(object sender, EventArgs e)
{
cx0 = -2.0;
cx1 = 0.5;
cy0 = -1.0;
cy1 = 1.0;
doDrawMandelbrot = true;
panelMain.Refresh();
}
And the Paint
event handler:
private void panelMain_Paint(object sender, PaintEventArgs e)
{
if (doDrawMandelbrot)
{
DrawMandelbrot(cx0, cy0, cx1, cy1, e.Graphics);
doDrawMandelbrot = false;
}
}
Points of Interest
The only way to stop flicker is to make your own class from panel that has only draws when you draw in Paint. This is done by setting the "style" which can't be done in a normal Panel
class.
public class MyPanel : System.Windows.Forms.Panel
{
public MyPanel()
{
this.SetStyle(
System.Windows.Forms.ControlStyles.UserPaint |
System.Windows.Forms.ControlStyles.AllPaintingInWmPaint |
System.Windows.Forms.ControlStyles.OptimizedDoubleBuffer,
true);
}
}
Once you have this new class set up, you have to change the declaration of the panel to use this new one. In forma1.designer.cs, change panelMain
from a normal panel to your new panel:
private void InitializeComponent()
{
this.buttonDraw = new System.Windows.Forms.Button();
this.buttonReset = new System.Windows.Forms.Button();
this.panelMain = new Mandelbrot.MyPanel();
and farther down..
private System.Windows.Forms.Button buttonDraw;
private System.Windows.Forms.Button buttonReset;
private MyPanel panelMain;
But that is only half the no flicker battle. Drawing the Mandelbrot takes seconds which would cause a bit of flicker if it had to do that each time we moved the mouse for the selection rectangle. So we save it to a bitmap, draw to a bitmap, then copy the bitmap to the visible bitmap as we move the mouse. No flicker.
So mouse move event looks like:
private void panelMain_MouseMove(object sender, MouseEventArgs e)
{
if (isMouseDown)
{
mx1 = e.X;
my1 = e.Y;
panelMain.Refresh();
}
}
and the paint now looks like this:
private void panelMain_Paint(object sender, PaintEventArgs e)
{
Graphics g;
if (isMouseDown)
{
Pen penYellow = new Pen(Color.ForestGreen);
bMap = (Bitmap )bMapSaved.Clone();
g = Graphics.FromImage(bMap);
g.DrawRectangle(penYellow, mx0, my0, mx1-mx0, my1-my0);
e.Graphics.DrawImageUnscaled(bMap, 0, 0); }
else
{
if (doDrawMandelbrot)
{
g = Graphics.FromImage(bMap);
DrawMandelbrot(cx0, cy0, cx1, cy1, g);
e.Graphics.DrawImageUnscaled(bMap, 0, 0);
bMapSaved = (Bitmap)bMap.Clone();
doDrawMandelbrot = false;
}
else
{
e.Graphics.DrawImageUnscaled(bMap, 0, 0);
}
}
}
The other point is how to scale/zoom. The main drawing function, not shown, takes as parameters which part of the set to draw. So all you have to do is convert the points you select with the mouse to the points in the set:
private void panelMain_MouseUp(object sender, MouseEventArgs e)
{
isMouseDown = false;
mx1 = e.X;
my1 = e.Y;
double scaleX, scaleY;
scaleX = (cx1 - cx0) / (double )panelMain.Width;
scaleY = (cy1 - cy0) / (double)panelMain.Height;
cx1 = (double )mx1*scaleX + cx0;
cy1 = (double)my1*scaleY + cy0;
cx0 = (double)mx0 * scaleX + cx0;
cy0 = (double)my0 * scaleY + cy0;
doDrawMandelbrot = true;
panelMain.Refresh(); }
And we don't want horrible banded colours, we want nice smooth colours. This odd bit of math makes a "smooth" number between 0 and 1 which we can use as the hue to get a nice colour. If you look at the math link it looks very complex, but it boils down to this:
private Color MapColor(int i, double r, double c)
{
double di=(double )i;
double zn;
double hue;
zn = Math.Sqrt(r + c);
hue = di + 1.0 - Math.Log(Math.Log(Math.Abs(zn))) / Math.Log(2.0);
hue = 0.95 + 20.0 * hue; while (hue > 360.0)
hue -= 360.0;
while (hue < 0.0)
hue += 360.0;
return ColorFromHSV(hue, 0.8, 1.0);
}
Please see the full source for a working version, and all the details.
History