基于C#窗体+GDI+绘图实现分形树
实验:分形树
一. 引言
实验目的:继续学习C#窗体应用程序的图形化界面设计以及GDI+绘图的一些基本指示,通过制作各种类型的分形树增强对于递归的理解,在创造分形图形的过程中感受编程的快乐
Tutorial任务
1.制作不同类型的分形图形(本次演示的是两种不同类型的分形树)
2.设计一个美观合理的UI界面
3.巩固关于打开文件和保存文件的操作
二.实验环境
Windows系统下的visual studio 2017
C#窗体应用程序
三.实验流程
1.实验原理介绍
首先,我们先介绍一下分形图形的绘制过程(由于分形图形的种类有很多种,但大多数是采用了递归的思想进行绘制,所以本文以较为常见和相对来讲难度不那么大的分形树进行讲解,感兴趣的读者可以尝试去制作更为复杂的分形图形)
首先,我们先来看一段分形树绘制的核心代码
private void paintFractalTree(Pen pen, float xo, float yo, int depth, double dangle, float length, double angle, Graphics line)
{float yf = yo - (sin(Converttohudu(dangle)) * length);//sin函数将角度转换成弧度float xf = xo - (cos(Converttohudu(dangle)) * length);line.DrawLine(pen, xo, yo, xf, yf);paintFractalTree(pen2, xf, yf, depth - 1, dangle + angle, length, angle, line);paintFractalTree(pen3, xf, yf, depth - 1, dangle - angle, length, angle, line);//画左分支
}
在上述的代码中我们已经看到了递归的影子,接下来会对递归流程进行详细介绍:
百度百科当中关于分形这个几何学术语的解释如下:
分形,具有以非整数维形式充填空间的形态特征。通常被定义为“一个粗糙或零碎的几何形状,可以分成数个部分,且每一部分都(至少近似地)是整体缩小后的形状”,即具有自相似的性质。
分形使人们觉悟到科学与艺术的融合,数学与艺术审美上的统一,使从前枯燥的数学不再仅仅是抽象的哲理,而是具体的感受;不再仅仅是揭示一类存在,而是一种艺术创作,分形搭起了科学与艺术的桥梁。
通过"每一部分都(至少近似地)是整体缩小后的形状”,即具有自相似的性质。这一点也能够看出做出分形图案的主要思想就是递归.
我们先考虑当分形树的分形元是下述图案的情况:
这个分形元在C#中是怎么绘制出来的呢?
1.首先,我们设A点坐标为(x,y),B点坐标为(x0,y0)
2.接下来绘制AB这条直线,也就是树的主干部分,即(x,y)--(x0,y0)直线,同时注意到,AB这条线段与横轴的夹角为90度,那么我们就以这个90度作为基础,结合用户输入的angle角度的可调整值,来绘制接下来的分形树分支
3.假设C点坐标为(x1,y1),则由三角函数的性质,结合上图中的α角我们可以看出C点的横坐标为x0-L'×cos(90°-α)(注意,这里面的L'指的是枝干的长度,为了让我们的分形树绘制的更加美观,这里面取枝干长度L'=2L/3,α就是用户输入的角度值)
4.类比3的思路,C点的y坐标y1为y0-L'×sin(90°-α)(值得注意的是,绘图板的左上角为(0,0),所以在向上绘制的时候相应的y坐标就会减少)
5.结合刚才的思想,我们很容易根据图中的提示绘制出右侧的分支,D点的横坐标为x0-L'×cos(90°+α),D点的纵坐标为y0-L'×sin(90°+α),如上图所示
6.连接BC,BD,分形元绘制完成
2.实验过程
这个时候我们回过头来看刚才的那段代码,就显得清楚多了
private void paintFractalTree(Pen pen, float xo, float yo, int depth, double dangle, float length, double angle, Graphics line)//dangle是目前的角度,也就是刚才说的(90+angle)或者(90-angle)传入作为dangle(其实就是需要计算正余弦的角度)
{float yf = yo - (sin(Converttohudu(dangle)) * length);//sin函数将角度转换成弧度,算出新的点所应该在的位置的纵坐标float xf = xo - (cos(Converttohudu(dangle)) * length);//算出新的点所应该在的位置的横坐标line.DrawLine(pen, xo, yo, xf, yf);//将原来点和新的点之间连一条线paintFractalTree(pen2, xf, yf, depth - 1, dangle + angle, length, angle, line);//递归画右分支paintFractalTree(pen3, xf, yf, depth - 1, dangle - angle, length, angle, line);//画左分支
}
//对于递归部分而言,需要注意再下一次递归的时候初始角度相当于不再是90度了,而是90-a(如果是左分支的话),或者是(90+a)(如果是右分支的话),以后每次递归都需要更新前面的角度值(也就是每次传入参数的dangle),通过设置递归深度可以绘制出如下的美丽图形:
注:读者可以在上述基础上做出创新,比如给这颗分形树绘制不同的颜色,让不同的分支长度不同等等,在这里我绘制了一种随机颜色的分形树,效果和代码部分如下:
private void paintFractalTree(Pen pen, float xo, float yo, int depth, double dangle, float length, double angle, Graphics line){if (depth > -1){length = length * 2 / 3;float yf = yo - (sin(Converttohudu(dangle)) * length);//转换成弧度float xf = xo - (cos(Converttohudu(dangle)) * length);line.DrawLine(pen, xo, yo, xf, yf);Random rnd = new Random();// 随机色生成,可以画出彩色的树Color myColor1 = Color.FromArgb(rnd.Next(0, 255), /*红色*/rnd.Next(0, 255), /*绿色*/rnd.Next(0, 255) /*蓝色*/);Pen pen2 = new Pen(myColor1);paintFractalTree(pen2, xf, yf, depth - 1, dangle + angle, length, angle, line);//画右分支Color myColor2 = Color.FromArgb(rnd.Next(0, 255), /*红色*/rnd.Next(0, 255), /*绿色*/rnd.Next(0, 255) /*蓝色*/);Pen pen3 = new Pen(myColor2);paintFractalTree(pen3, xf, yf, depth - 1, dangle - angle, length, angle, line);//画左分支}}
几个较为美观的色彩图如下图所示:
通过不同的angle角度的设置,可以产生许多好看的图形,以上为30度和60度以及90度设置下的图形,读者可以自行尝试其他方案,绘制更好看的分形树
3.实验创新
绘制出了上述的最简单的分形树之后,我们也许会思考其他分形元是不是会画出其他神奇的分形图案?答案是肯定的,接下来我会以我绘制的第二种分形树为例,向读者展示绘制不同种分形树的一般方法:
首先来看第二种分形树的效果图:
细心的读者可能已经发现了,实际上绘制一颗分形树的要点就在于一定要掌握好递归的思想,我们简单看一下上图就不难分析出分形元是如下这种图形了:
再分解一下其实就是图中的黑色部分了,也就是我们首先要绘制出黑色的部分,具体的原理读者可以自行尝试一下,在这里直接给出核心代码:
private void paintFractalTree2(Pen pen, float xo, float yo, int depth, double dangle, float length, double angle, Graphics line){double attach = 50;if (depth > -1){float changelength = length * 2 / 3;float x1 = xo - (cos(Converttohudu(dangle))) * length;float y1 = yo- (sin(Converttohudu(dangle)))*length;float x2 = x1 - (cos(Converttohudu(dangle))) * changelength;float y2 = y1 - (sin(Converttohudu(dangle))) * changelength;float x2r = x1 - (cos(Converttohudu(dangle+angle))) * changelength;float y2r = y1 - (sin(Converttohudu(dangle+angle))) * changelength;float x2l = x1 - (cos(Converttohudu(dangle -angle))) * changelength;float y2l = y1 - (sin(Converttohudu(dangle - angle))) * changelength;line.DrawLine(pen, xo, yo, x1, y1);//line.DrawLine(pen, x1, y1, x2, y2);line.DrawLine(pen, x1, y1, x2r, y2r);//line.DrawLine(pen, x1, y1, x2l, y2l);Random rnd = new Random();// 随机色生成Color myColor1 = Color.FromArgb(rnd.Next(0, 255), /*红色*/rnd.Next(0, 255), /*绿色*/rnd.Next(0, 255) /*蓝色*/);Pen pen2 = new Pen(myColor1);paintFractalTree2(pen2, x2r, y2r, depth - 1, dangle + angle*3/2 + angle, length*2/3, angle, line);//画右分支,这里让角度发生了变化,起到一个图形"折起来"的效果paintFractalTree2(pen2, x2r, y2r, depth - 1, dangle + angle * 3 / 2 - angle, length*2/3, angle, line);//画左分支//paintFractalTree2(pen2, x2l, y2l, depth - 1, dangle - angle * 3 / 2 - angle, length*2/3,
//这句话加上去又是一种新的图案了,可以试试?Color myColor2 = Color.FromArgb(rnd.Next(0, 255), /*红色*/rnd.Next(0, 255), /*绿色*/rnd.Next(0, 255) /*蓝色*/);Pen pen3 = new Pen(myColor2);}}
到这里,我们的两种分形树就绘制完成了,可以结合之前的画图软件的操作将我们喜欢的分形树图案保存起来,也可以打开之前绘制好的分形树图案,绘制的窗体界面如下:
总的代码如下图所示(窗体设计部分已省去)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
using System.IO;namespace FractalTree
{public partial class Form1 : Form{bool click1 = false;bool click2 = false;//文件名public string curFileName= @"E:\期末考试复习\C#\分形树v3.0\FractalTree\FractalTree\bin\Debug\picture.jpg";//图像对象private System.Drawing.Bitmap curBitmap;//public Bitmap bmp = new Bitmap(769, 650);public Form1(){InitializeComponent();this.MaximizeBox = false;this.Size = new Size(769, 650);this.CenterToScreen();this.FormBorderStyle = FormBorderStyle.FixedSingle;picturebox.BorderStyle = BorderStyle.Fixed3D;this.Text = "分形树";curBitmap = (Bitmap)Image.FromFile(curFileName);//Graphics g = Graphics.FromImage(bmp);//picturebox.Image = ;//curBitmap = (Bitmap)bmp;}public void pictureBox_Paint(object sender, PaintEventArgs e){//Graphics g = this.CreateGraphics();//picturebox.Refresh();if (curBitmap != null){//使用DrawImage的方法绘制图像Graphics g = e.Graphics;//curBitmap.Width, curBitmap.Height图像的宽度和高度g.DrawImage(curBitmap, 0, 0, curBitmap.Width, curBitmap.Height);}if (click1){//picturebox.Refresh();//Graphics g = e.Graphics;//if(curFileName!=null)curBitmap = (Bitmap)Image.FromFile(curFileName);//else// curBitmap = bmp;this.picturebox.Image = curBitmap;Graphics g = Graphics.FromImage(this.picturebox.Image);Pen pen = new Pen(Color.Black);float length = Convert.ToSingle(tb1.Text) * 10;double angle = Convert.ToDouble(nud3.Value);int depth = Convert.ToInt32(nud1.Value);float xo = (picturebox.Width / 2) - 2;float yo = picturebox.Height - Convert.ToSingle(5.4); paintFractalTree(pen, xo, yo, depth, 90, length, angle, g);//那个90是确定这颗树往哪个方向开始长的pen.Dispose();click1 = false;}else if(click2){//picturebox.Refresh();if (curFileName != null)curBitmap = (Bitmap)Image.FromFile(curFileName);//else// curBitmap = bmp;this.picturebox.Image = curBitmap;Graphics g = Graphics.FromImage(this.picturebox.Image);//Graphics g = e.Graphics;Pen pen = new Pen(Color.Black);float length = Convert.ToSingle(tb1.Text) * 10;double angle = Convert.ToDouble(nud3.Value);int depth = Convert.ToInt32(nud1.Value);float xo = (picturebox.Width / 2) - 2;float yo = picturebox.Height - Convert.ToSingle(5.4);paintFractalTree2(pen, xo, yo, depth, 90, length*1/2, angle, g);//那个90是确定这颗树往哪个方向开始长的pen.Dispose();click2 = false;}}private void paintFractalTree(Pen pen, float xo, float yo, int depth, double dangle, float length, double angle, Graphics line){if (depth > -1){length = length * 2 / 3;float yf = yo - (sin(Converttohudu(dangle)) * length);//转换成弧度float xf = xo - (cos(Converttohudu(dangle)) * length);line.DrawLine(pen, xo, yo, xf, yf);Random rnd = new Random();// 随机色生成Color myColor1 = Color.FromArgb(rnd.Next(0, 255), /*红色*/rnd.Next(0, 255), /*绿色*/rnd.Next(0, 255) /*蓝色*/);Pen pen2 = new Pen(myColor1);paintFractalTree(pen2, xf, yf, depth - 1, dangle + angle, length, angle, line);//画右分支Color myColor2 = Color.FromArgb(rnd.Next(0, 255), /*红色*/rnd.Next(0, 255), /*绿色*/rnd.Next(0, 255) /*蓝色*/);Pen pen3 = new Pen(myColor2);paintFractalTree(pen3, xf, yf, depth - 1, dangle - angle, length, angle, line);//画左分支}}private void paintFractalTree2(Pen pen, float xo, float yo, int depth, double dangle, float length, double angle, Graphics line){double attach = 50;if (depth > -1){float changelength = length * 2 / 3;float x1 = xo - (cos(Converttohudu(dangle))) * length;float y1 = yo- (sin(Converttohudu(dangle)))*length;float x2 = x1 - (cos(Converttohudu(dangle))) * changelength;float y2 = y1 - (sin(Converttohudu(dangle))) * changelength;float x2r = x1 - (cos(Converttohudu(dangle+angle))) * changelength;float y2r = y1 - (sin(Converttohudu(dangle+angle))) * changelength;float x2l = x1 - (cos(Converttohudu(dangle -angle))) * changelength;float y2l = y1 - (sin(Converttohudu(dangle - angle))) * changelength;line.DrawLine(pen, xo, yo, x1, y1);//line.DrawLine(pen, x1, y1, x2, y2);line.DrawLine(pen, x1, y1, x2r, y2r);//line.DrawLine(pen, x1, y1, x2l, y2l);Random rnd = new Random();// 随机色生成Color myColor1 = Color.FromArgb(rnd.Next(0, 255), /*红色*/rnd.Next(0, 255), /*绿色*/rnd.Next(0, 255) /*蓝色*/);Pen pen2 = new Pen(myColor1);paintFractalTree2(pen2, x2r, y2r, depth - 1, dangle + angle*3/2 + angle, length*2/3, angle, line);//画右分支paintFractalTree2(pen2, x2r, y2r, depth - 1, dangle + angle * 3 / 2 - angle, length*2/3, angle, line);//画右分支//paintFractalTree2(pen2, x2l, y2l, depth - 1, dangle - angle * 3 / 2 - angle, length*2/3, angle, line);//画右分支//paintFractalTree2(pen2, x2l, y2l, depth - 1, dangle - angle * 3 / 2 + angle, length * 2 / 3, angle, line);//画右分支Color myColor2 = Color.FromArgb(rnd.Next(0, 255), /*红色*/rnd.Next(0, 255), /*绿色*/rnd.Next(0, 255) /*蓝色*/);Pen pen3 = new Pen(myColor2);}}private float cos(double angle){return (float)Math.Cos(angle);}//求cos的值private float sin(double angle){return (float)Math.Sin(angle);}//求sin的值private double Converttohudu(double angle){return (Math.PI * angle) / 180;}//将角度转换成弧度private void textBox1_KeyPress(object sender, KeyPressEventArgs e){if (!Char.IsDigit(e.KeyChar) && !Char.IsControl(e.KeyChar) && !e.KeyChar.Equals(',')){e.Handled = true;//可以从文本框中获取键盘读入的数据}}private void drawbt_Click(object sender, EventArgs e){click1 = true;picturebox.Refresh();}private void nud3_ValueChanged(object sender, EventArgs e){}private void nud1_ValueChanged(object sender, EventArgs e){}private void Form1_Load(object sender, EventArgs e){}private void 打开ToolStripMenuItem_Click(object sender, EventArgs e){OpenFileDialog opnDlg = new OpenFileDialog();opnDlg.Filter = "所有图像文件 | *.bmp; *.pcx; *.png; *.jpg; *.gif;" +"*.tif; *.ico; *.dxf; *.cgm; *.cdr; *.wmf; *.eps; *.emf|" +"位图( *.bmp; *.jpg; *.png;...) | *.bmp; *.pcx; *.png; *.jpg; *.gif; *.tif; *.ico|" +"矢量图( *.wmf; *.eps; *.emf;...) | *.dxf; *.cgm; *.cdr; *.wmf; *.eps; *.emf";opnDlg.Title = "打开图像文件";opnDlg.ShowHelp = true;if (opnDlg.ShowDialog() == DialogResult.OK){curFileName = opnDlg.FileName;try{curBitmap = (Bitmap)Image.FromFile(curFileName);}catch (Exception exp){MessageBox.Show(exp.Message);}}//对窗体进行重新绘制,这将强制执行paint事件处理程序Invalidate();picturebox.Refresh();}//private void Form1_Paint(object sender, PaintEventArgs e)//{// //使用窗体的Paint事件的PaintEventArgs属性来获取一个与窗体相关联的Graphic对象。// Graphics g = e.Graphics;// if (curBitmap != null)// {// //使用DrawImage的方法绘制图像// //160,20 :显示在主窗体内,图像左上角的坐标// //curBitmap.Width, curBitmap.Height图像的宽度和高度// g.DrawImage(curBitmap, 160, 20, curBitmap.Width, curBitmap.Height);// }//}public void 保存ToolStripMenuItem_Click(object sender, EventArgs e ){ }private void button1_Click(object sender, EventArgs e){click2 = true;picturebox.Refresh();}private void 保存ToolStripMenuItem_Click_1(object sender, EventArgs e){if (curBitmap == null){return;}Graphics g = Graphics.FromImage(curBitmap);SaveFileDialog saveDlg = new SaveFileDialog();saveDlg.Title = "保存为";saveDlg.OverwritePrompt = true;saveDlg.Filter ="BMP文件 (*.bmp) | *.bmp|" +"Gif文件 (*.gif) | *.gif|" +"JPEG文件 (*.jpg) | *.jpg|" +"PNG文件 (*.png) | *.png";saveDlg.ShowHelp = true;if (saveDlg.ShowDialog() == DialogResult.OK){string fileName = saveDlg.FileName;string strFilExtn = fileName.Remove(0, fileName.Length - 3);switch (strFilExtn){case "bmp":curBitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Bmp);break;case "jpg":curBitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Jpeg);break;case "gif":curBitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Gif);break;case "tif":curBitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Tiff);break;case "png":curBitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Png);break;default:break;}}}private void 另存为ToolStripMenuItem_Click(object sender, EventArgs e){if (curBitmap == null){return;}Graphics g = Graphics.FromImage(curBitmap);SaveFileDialog saveDlg = new SaveFileDialog();saveDlg.Title = "保存为";saveDlg.OverwritePrompt = true;saveDlg.Filter ="BMP文件 (*.bmp) | *.bmp|" +"Gif文件 (*.gif) | *.gif|" +"JPEG文件 (*.jpg) | *.jpg|" +"PNG文件 (*.png) | *.png";saveDlg.ShowHelp = true;if (saveDlg.ShowDialog() == DialogResult.OK){string fileName = saveDlg.FileName;string strFilExtn = fileName.Remove(0, fileName.Length - 3);switch (strFilExtn){case "bmp":curBitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Bmp);break;case "jpg":curBitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Jpeg);break;case "gif":curBitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Gif);break;case "tif":curBitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Tiff);break;case "png":curBitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Png);break;default:break;}}}}}
至此,我们的分形树以及相关代码实现就完成了。