实验要求

目的:熟悉图形化界面程序,开发有趣应用。

2.2 参考分形的概念,绘制分形树或者其他分形图形。要求可以对图形进行保存和打开等操作。

需求分析

抛开分形树不看,这次的实验就是简单的图片浏览器,并且无需编辑功能,只需实现读写即可。这部分内容我们其实已经在实验#2实现过,本质上就是调用C#的自带控件。所以本次实验的重点即为 分形树 的绘制。

分形树

分形(Fractal),具有以非整数维形式充填空间的形态特征。通常被定义为“一个粗糙或零碎的几何形状,可以分成数个部分,且每一部分都(至少近似地)是整体缩小后的形状”,即具有自相似的性质。

而分形树,其基本单元就是一棵“Y”型的“树”,再将这棵树的两个分叉分别作为另外两棵树的根,以此不断扩张。换句话说,类似于一棵上下颠倒的 满二叉树

结合与其结构类似的满二叉树,可以想到,我们应该用 递归 来实现分形树。

代码实现

定义变量

1
2
3
4
5
6
7
Graphics graphics;  //画板
Bitmap paintImg; //绘画图像
int pWidth; //图像宽度
int pHeight; //图像高度
Pen pen; //笔刷
float penWidth = 5.0f; //初始宽度
float drawLength = 100.0f; //初始长度

graphicspaintImg 变量用来绘制分形树并将其显示在屏幕上。而 penWidthdrawLength 变量则是用于限定整棵分形树的根的宽度与长度的。

初始化画布

1
2
3
4
5
6
7
8
9
10
11
12
pWidth = pictureBox.Width;
pHeight = pictureBox.Height;

//创建一个空白画布
paintImg = new Bitmap(pWidth, pHeight);
graphics = Graphics.FromImage(paintImg);
graphics.Clear(Color.Yellow);
pictureBox.Image = paintImg;

graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; //抗锯齿

//Draw_FractalTree()

由于要在程序中实时显示图片,所以我们需要一个 PictureBox 控件来存放当前图片。然后用该控件的大小创建一个空的 Bitmap ,来作为绘制分形树的图片,并将其置于画布上,然后将背景设为一个好看的颜色。最终将其显示在PictureBox 上。由于画布的默认线条较为离散,我们需要一行 SmoothingMode 的设置对其进行抗锯齿化。

绘制分形树

1
private void Draw_FractalTree(Point start, int fractalTimes,float width,float length,double angle,int colorA) { }

前文已经提到过,绘制分形树的函数部分是一个递归函数,所以我们先将其结构写出来。我们来分析一下分形树的每部分基础结构所需要的参数:

  1. 起点 start 用于确定当前子树的根的起点坐标,我们从这个点开始进行绘制。
  2. fractalTimes 是作为递归次数的标记,由于递归函数总是需要一个终点的,我们就设置一个递归次数标记,当其值为 0 时即结束绘制。
  3. widthlengthangle 则分别对应当前子树的笔刷宽度,根长度与偏转角。为了让我们的树更加美观,这三个参数在每次绘制中都应该做少许改变。
  4. colorA 则是作为笔刷颜色的透明度参数,这个参数只是用来使得树看起来更美观,不是必要存在。

首先先为递归设置好退出条件,使绘制部分拥有出口:if (fractalTimes == 0) return;

接下来就可以设置笔刷的基本参数然后绘制线条了:

1
2
3
4
5
pen = new Pen(Color.FromArgb(colorA, 0, 0, 0), width);
pen.StartCap = LineCap.Round;
pen.EndCap = LineCap.Round;
Point end = new Point(start.X + (int)(length * Math.Sin(angle)), start.Y - (int)(length * Math.Cos(angle)));
graphics.DrawLine(pen, start, end);

以上内容绘制了二叉中的其中一叉,绘制完后即可从该线段的终点为下一棵子树的起点继续进行绘制,并且对参数做一些细微改变:

1
Draw_FractalTree(end, fractalTimes - 1, width * 0.8f, length * 0.8f, angle + Math.PI / 8, colorA - 20);

同理,我们可以画出另外一叉:

1
2
3
end = new Point(start.X + (int)(length * Math.Sin(angle)), start.Y - (int)(length * Math.Cos(angle)));
graphics.DrawLine(pen, start, end);
Draw_FractalTree(end, fractalTimes - 1, width * 0.8f, length * 0.8f, angle - Math.PI / 8, colorA - 20);

最后我们在主体函数中调用该函数,并设置好基础参数,就可以运行了:

1
Draw_FractalTree(new Point(pWidth / 2, pHeight), 12, penWidth, drawLength, 0, 255);

打开/保存文件

打开图像可以直接调用C#的控件 OpenFileDialog ,同样,保存图像可以调用 SaveFileDialog 。由于我们处理的是图像文件,所以把 DefaultExt(默认扩展名)一项设置为 PNG ;把 Filter(文件筛选器)设置为 PNG(\*.png)|\*.png|JPEG(\*.jpg;\*.jpeg)|\*.jpg

接着可以创建一个 MenuStrip 套入这两个功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void 打开OToolStripMenuItem1_Click(object sender, EventArgs e)
{
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
paintImg = (Bitmap)Image.FromFile(openFileDialog.FileName);
pictureBox.Image = paintImg;
}
}

private void 保存SToolStripMenuItem_Click(object sender, EventArgs e)
{
if (saveFileDialog.ShowDialog() == DialogResult.OK)
{
paintImg.Save(saveFileDialog.FileName, ImageFormat.Png);
}
}

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Windows.Forms;

namespace FractalTree
{
public partial class MainForm : Form
{
Graphics graphics; //画板
Bitmap paintImg; //绘画图像
int pWidth; //图像宽度
int pHeight; //图像高度
Pen pen; //笔刷
float penWidth = 5.0f; //初始宽度
float drawLength = 100.0f; //初始长度

public MainForm()
{
InitializeComponent();
}

private void MainForm_Load(object sender, EventArgs e)
{
pWidth = pictureBox.Width;
pHeight = pictureBox.Height;

//创建一个空白画布
paintImg = new Bitmap(pWidth, pHeight);
graphics = Graphics.FromImage(paintImg);
graphics.Clear(Color.Yellow);
pictureBox.Image = paintImg;

graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; //抗锯齿

Draw_FractalTree(new Point(pWidth / 2, pHeight), 12, penWidth, drawLength, 0, 255);
}

private void Draw_FractalTree(Point start, int fractalTimes,float width,float length,double angle,int colorA)
{
if (fractalTimes == 0) return;
pen = new Pen(Color.FromArgb(colorA, 0, 0, 0), width);
pen.StartCap = LineCap.Round;
pen.EndCap = LineCap.Round;
Point end = new Point(start.X + (int)(length * Math.Sin(angle)), start.Y - (int)(length * Math.Cos(angle)));
graphics.DrawLine(pen, start, end);
Draw_FractalTree(end, fractalTimes - 1, width * 0.8f, length * 0.8f, angle + Math.PI / 8, colorA - 20);
end = new Point(start.X + (int)(length * Math.Sin(angle)), start.Y - (int)(length * Math.Cos(angle)));
graphics.DrawLine(pen, start, end);
Draw_FractalTree(end, fractalTimes - 1, width * 0.8f, length * 0.8f, angle - Math.PI / 8, colorA - 20);
}

private void 打开OToolStripMenuItem1_Click(object sender, EventArgs e)
{
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
paintImg = (Bitmap)Image.FromFile(openFileDialog.FileName);
pictureBox.Image = paintImg;
}
}

private void 保存SToolStripMenuItem_Click(object sender, EventArgs e)
{
if (saveFileDialog.ShowDialog() == DialogResult.OK)
{
paintImg.Save(saveFileDialog.FileName, ImageFormat.Png);
}
}

}
}