构造属于你的专属画图程序,可参考系统自带的绘图板
题目的要求非常简单,但是实现起来却是比较困难的。我们先观察和分析一下Windows自带的画图板来获得它们的基本功能,进而自己实现。
可以看到,系统自带的画图板最主要的功能就是这几个了,我们逐一分析并考虑如何来实现这几个功能。首先是文件:
可以看到它包含了新建画布,打开,保存,属性,关于和退出这几个功能。我们可以通过C#自带的文件处理或者GDI+自带的文件功能来完成,详情请往下看。
接着是画笔/工具、图形功能。这些功能在GDI+中都有相应的基础功能(Pen, Drawline等),我们到时后通过调用他们来完成。
最后是颜色和画笔的粗细,这些功能属于画图过程中的属性,我们需要用一些全局变量来保存它们。通过更改全局变量的值就可以实现不同模式的切换。
接下来我们来分析一下完成这个软件需要的类。注意,在实际开发的过程中我们并不需要先研究透这些类,而是在使用的过程中加以查询即可。
本题不存在太过复杂的数据结构,主要都是调用C#已有的类来实现功能。具体来说:
Bitmap类:位图,C#的图形类,派生自Image类。在这个实验中我们将在它的实例上作画。
Pen类:画图自然需要画笔,而Pen的实例就是一支画笔,它有粗细,颜色等功能,控制Pen实例的移动就能在位图上画出线条。
Point类:点,就是电脑屏幕上隐藏着的坐标。GDI+作画都要依靠坐标来实现。
Color类:颜色,到时候画图时使用到的颜色。颜色可使用它已有的内涵的枚举,或是通过ARGB值来指定。(A:不透明度,R, G, B:红绿蓝;ARGB是一个Int32的值,刚好4*8=32位)。
MessageBox类:消息提示框,用于显示简单的信息和互动窗口。
ColorDialog类:C#自带的调色板功能,可以通过它来实现调色,保存自定义颜色等。
SaveFileDialog类:C#自带的保存提示框功能,就是我们平时使用的软件第一次点击保存时会弹出来的那种框。
**(重头戏)**Graphics类:大名鼎鼎的GDI+,所有的绘图功能最终都是通过它的实例来实现在窗体或者画布上作画。
再次注意,要将这些类的功能全部研究完是非常麻烦的,我们只需要在使用的时候不断查询文档就可以了。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 真_画图板
{
public partial class Form1 : Form
{
#region //全局变量
Bitmap mypicture;//画布
Point a = new Point();
Point b = new Point();
Pen p = new Pen(Color.Black, 2);//画笔
Color mainColor = Color.Black, subColor = Color.White;//主副色,供选择
Color canvas = Color.White;//背景色
int pensize = 1;//笔宽
enum Mode { BrushMode, TextMode, RectangleMode, OvalMode };//供选择的工具模式
Mode mode = 0;//已选中的工具模式
bool startDrawing = false;//指示是否开始画图
bool created = false;//指示是否已创建图像
bool saved = false;//指示是否保存
#endregion
#region //窗口代码区
public Form1()
{
InitializeComponent();
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
//在已创建图像而未保存的情况下提醒用户
if (created && !saved)
{
if (MessageBox.Show("图片未保存,确定要退出程序吗?", "", MessageBoxButtons.OKCancel, MessageBoxIcon.Information) == DialogResult.OK)
;
else
e.Cancel = true;
}
}
#endregion
#region //画笔代码区
public void RecoverPen()//画笔复位
{
p.Width = pensize;
p.Color = mainColor;
this.Cursor = Cursors.Default;//鼠标箭头的形态恢复
}
private void toolStripButton1_Click(object sender, EventArgs e)
{
//铅笔
if (created)
{
mode = Mode.BrushMode;
RecoverPen();
label1.Text = "当前颜色1";
}
}
private void toolStripButton2_Click(object sender, EventArgs e)
{
//刷子
if (created)
{
mode = Mode.BrushMode;
RecoverPen();
p.Width = pensize * 2;
label1.Text = "当前颜色1";
}
}
private void toolStripButton3_Click(object sender, EventArgs e)
{
//橡皮
if (created)
{
mode = Mode.BrushMode;
p.Color = canvas;//背景色,本质上就是通过背景色来覆盖已经画好的线条
p.Width = pensize * 3;//大一点比较明显
this.Cursor = Cursors.SizeAll;//鼠标形状换个样式,目前还没有找到替换成自定义图片的方法
}
}
private void toolStripButton4_Click(object sender, EventArgs e)
{
//文字
if (created)
{
mode = Mode.TextMode;
}
}
private void toolStripButton6_Click(object sender, EventArgs e)
{
//矩形
if (created)
{
mode = Mode.RectangleMode;
RecoverPen();
}
}
private void toolStripButton7_Click(object sender, EventArgs e)
{
//椭圆
if (created)
{
mode = Mode.OvalMode;
RecoverPen();
}
}
#endregion
#region //颜色代码区
public Color colorBox()
{
ColorDialog colorDialog = new ColorDialog();
colorDialog.AllowFullOpen = true;
colorDialog.FullOpen = true;
colorDialog.Color = Color.Black;//设置默认颜色为黑
colorDialog.ShowHelp = true;
colorDialog.ShowDialog();//模式窗口
p.Color = colorDialog.Color;//获取选择后的颜色
return colorDialog.Color;
}
private void pictureBox2_Click(object sender, EventArgs e)
{
//单击切换颜色
p.Color = mainColor;
label1.Text = "当前颜色1";
}
private void pictureBox2_DoubleClick(object sender, EventArgs e)
{
//双击调出调色板
mainColor = colorBox();
Bitmap temp = new Bitmap(pictureBox2.Width, pictureBox2.Height);
Graphics g = Graphics.FromImage(temp);
g.Clear(mainColor);
pictureBox2.Image = temp;
label1.Text = "当前颜色1";
}
private void pictureBox3_Click(object sender, EventArgs e)
{
p.Color = subColor;
label1.Text = "当前颜色2";
}
private void pictureBox3_DoubleClick(object sender, EventArgs e)
{
subColor = colorBox();
Bitmap temp = new Bitmap(pictureBox3.Width, pictureBox3.Height);
Graphics g = Graphics.FromImage(temp);
g.Clear(subColor);
pictureBox3.Image = temp;
label1.Text = "当前颜色2";
}
#endregion
#region //粗细代码区
//通过一个Panel(当然用其他空间也可以)来将4个单选按钮集合在一起实现唯一的粗细
private void radioButton1_CheckedChanged(object sender, EventArgs e)
{
if (radioButton1.Checked)
{
pensize = 2;
p.Width = pensize;
}
}
private void radioButton2_CheckedChanged(object sender, EventArgs e)
{
if (radioButton2.Checked)
{
pensize = 4;
p.Width = pensize;
}
}
private void radioButton3_CheckedChanged(object sender, EventArgs e)
{
if (radioButton3.Checked)
{
pensize = 6;
p.Width = pensize;
}
}
private void radioButton4_CheckedChanged(object sender, EventArgs e)
{
if (radioButton4.Checked)
{
pensize = 8;
p.Width = pensize;
}
}
#endregion
#region //画布代码区
private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
//(无论什么模式)确定初始左键位置
if (e.Button == MouseButtons.Left)
{
startDrawing = true;
b.X = a.X = e.X;//b就是初始位置了,在下一次按下鼠标左键之前它不会改变
b.Y = a.Y = e.Y;
if (mode == Mode.TextMode)//特殊处理
{
Form2 textBox = new Form2();//创建一个含富文本框的新窗口用于输入要画的文字
textBox.ShowDialog();
Graphics g = Graphics.FromImage(mypicture);
g.DrawString(PublicVal.textString, new Font("宋体", 12), new SolidBrush(Color.Black), b);
//Drawstring函数用来将输入的文本绘制到画布上,它的第一个参数就是我们的全局变量
pictureBox1.Image = mypicture;
g.Dispose();
}
}
}
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
if (startDrawing == true && e.Button == MouseButtons.Left)//确定是左键
{
Graphics g = Graphics.FromImage(mypicture);
//调出初始画布,这块画布是在鼠标移动的每一帧中不停叠加更新的
switch (mode)
{
case (Mode.BrushMode):
{
g.DrawLine(p, a.X, a.Y, e.X, e.Y); break;
//画短线模拟连续移动的过程,这是整个画图软件最精髓的一行代码
}
case (Mode.RectangleMode):
{
//将原来的矩形通过背景色覆盖掉之后再绘制画笔色的新矩形,以实现动画的效果,最终确定下来的矩形在MouseUp事件中被确定
//(存在将已作的图覆盖掉的bug)
g.DrawRectangle(new Pen(canvas, pensize), b.X, b.Y, a.X - b.X, a.Y - b.Y);
g.DrawRectangle(p, b.X, b.Y, e.X - b.X, e.Y - b.Y);
break;
}
case (Mode.OvalMode):
{
g.DrawEllipse(new Pen(canvas, pensize), b.X, b.Y, a.X - b.X, a.Y - b.Y);
g.DrawEllipse(p, b.X, b.Y, e.X - b.X, e.Y - b.Y);
break;
}
default:
break;
}
pictureBox1.Image = mypicture;//将新画出来的图在picturebox中显示
g.Dispose();//释放画笔资源
a.X = e.X;//更新点位置
a.Y = e.Y;
}
}
private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
//(无论什么模式)确定结束位置
Graphics g = Graphics.FromImage(mypicture);
switch (mode)
{
case (Mode.RectangleMode):
{
g.DrawRectangle(p, b.X, b.Y, a.X - b.X, a.Y - b.Y); break;
}
case (Mode.OvalMode):
{
g.DrawEllipse(p, b.X, b.Y, a.X - b.X, a.Y - b.Y); break;
}
default:
break;
}
pictureBox1.Image = mypicture;
g.Dispose();
startDrawing = false;
}
#endregion
#region //菜单栏代码区
private void 新建ToolStripMenuItem_Click(object sender, EventArgs e)
{
if (created)
{
if (MessageBox.Show("图片未保存,确定要新建吗?", "", MessageBoxButtons.OKCancel, MessageBoxIcon.Information) == DialogResult.OK)
;
}
else
{
created = true;
pictureBox1.Visible = true;//在新建完成之后才能将画布在picturebox中显示出来
mypicture = new Bitmap(pictureBox1.Width, pictureBox1.Height);
Graphics g = Graphics.FromImage(mypicture);
g.Clear(canvas);//用背景色清空画布
pictureBox1.Image = mypicture;
}
}
private void 设置背景色ToolStripMenuItem_Click(object sender, EventArgs e)
{
if (created && !saved)//是否保存当前?
{
if (MessageBox.Show("图片未保存,确定要新建吗?", "", MessageBoxButtons.OKCancel, MessageBoxIcon.Information) == DialogResult.OK)
;
else
return;
}
ColorDialog colorDialog = new ColorDialog();
colorDialog.AllowFullOpen = true;
colorDialog.FullOpen = true;
colorDialog.Color = Color.Black;
colorDialog.ShowHelp = true;
colorDialog.ShowDialog();
canvas = colorDialog.Color;
mypicture = new Bitmap(pictureBox1.Width, pictureBox1.Height);
Graphics g = Graphics.FromImage(mypicture);
g.Clear(canvas);
pictureBox1.Image = mypicture;
return;
}
private void 保存ToolStripMenuItem_Click(object sender, EventArgs e)
{
if (!created)
{
MessageBox.Show("未创建图像!");
return;
}
SaveFileDialog save = new SaveFileDialog();
//saveBox的详细功能请咨询文档
save.Filter = "Jpg 图片|*.jpg|Bmp 图片|*.bmp|Gif 图片|*.gif|Png 图片|*.png|Wmf 图片|*.wmf";
save.FilterIndex = 1;
save.DefaultExt = ".jpg";
save.FileName = "无标题_picture";
if (save.ShowDialog() == DialogResult.OK)
{
mypicture.Save(save.FileName);
}
saved = true;
}
private void 退出ToolStripMenuItem_Click(object sender, EventArgs e)
{
//关闭当前窗口
this.Close();
}
private void 帮助ToolStripMenuItem_Click(object sender, EventArgs e)
{
//弹出帮助信息框
Form3 help = new Form3();
help.ShowDialog();
}
private void 关于ToolStripMenuItem_Click(object sender, EventArgs e)
{
//弹出作者的信息
Form4 about = new Form4();
about.ShowDialog();
}
#endregion
}
//全局变量区
public class PublicVal
{
public static string textString = "";
}
}
Bitmap mypicture;//画布
Point a = new Point();
Point b = new Point();
Pen p = new Pen(Color.Black, 2);//画笔
Color mainColor = Color.Black, subColor = Color.White;//主副色,供选择
Color canvas = Color.White;//背景色
int pensize = 1;//笔宽
enum Mode { BrushMode, TextMode, RectangleMode, OvalMode };//供选择的工具模式
Mode mode = 0;//已选中的工具模式
bool startDrawing = false;//指示是否开始画图
bool created = false;//指示是否已创建图像
bool saved = false;//指示是否保存
//这个类在最后
public class PublicVal
{
public static string textString = "";
}
这里解释一下点a和b。点a是较为通用的点,在画笔移动的过程中需要不停被更替。而点b则是记录鼠标点击画布后的那个位置,不会轻易更变除非一次绘画(一笔)结束。
最后的全局变量用于富文本框窗口和主窗口之间的信息传递,只会在文字模式中用到,到时候我们再来看。需要提到的是,由于C#中不存在实际上的全局变量,于是我们通过同一个命名空间中的公有类的静态变量来实现相应的功能。
public void RecoverPen()//画笔复位
{
p.Width = pensize;
p.Color = mainColor;
this.Cursor = Cursors.Default;//鼠标箭头的形态恢复
}
private void toolStripButton1_Click(object sender, EventArgs e)
{
//铅笔
if (created)
{
mode = Mode.BrushMode;
RecoverPen();
label1.Text = "当前颜色1";
}
}
在每次状态变更回来之后(比如切换到了橡皮擦再切换回铅笔,其他工具同理)都需要将画笔恢复到最基本的状态。选中工具之后,将模式mode切换到相应的工具,并通过画布模块来进行作画。
public Color colorBox()
{
ColorDialog colorDialog = new ColorDialog();
colorDialog.AllowFullOpen = true;
colorDialog.FullOpen = true;
colorDialog.Color = Color.Black;//设置默认颜色为黑
colorDialog.ShowHelp = true;
colorDialog.ShowDialog();//模式窗口
p.Color = colorDialog.Color;//获取选择后的颜色
return colorDialog.Color;
}
private void pictureBox2_Click(object sender, EventArgs e)
{
//单击切换颜色
p.Color = mainColor;
label1.Text = "当前颜色1";
}
private void pictureBox2_DoubleClick(object sender, EventArgs e)
{
//双击调出调色板
mainColor = colorBox();
Bitmap temp = new Bitmap(pictureBox2.Width, pictureBox2.Height);
Graphics g = Graphics.FromImage(temp);
g.Clear(mainColor);
pictureBox2.Image = temp;
label1.Text = "当前颜色1";
}
从这里就可以看出ColorDialog的用途了,最重要的就是Color属性,它代表了用户对调色器操作之后的颜色。画图板的界面如下:
点击确定之后Color属性就被改变了。
private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
//(无论什么模式)确定初始左键位置
if (e.Button == MouseButtons.Left)
{
startDrawing = true;
b.X = a.X = e.X;//b就是初始位置了,在下一次按下鼠标左键之前它不会改变
b.Y = a.Y = e.Y;
if (mode == Mode.TextMode)//特殊处理
{
Form2 textBox = new Form2();//创建一个含富文本框的新窗口用于输入要画的文字
textBox.ShowDialog();
Graphics g = Graphics.FromImage(mypicture);
g.DrawString(PublicVal.textString, new Font("宋体", 12), new SolidBrush(Color.Black), b);
//Drawstring函数用来将输入的文本绘制到画布上,它的第一个参数就是我们的全局变量
pictureBox1.Image = mypicture;
g.Dispose();
}
}
}
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
if (startDrawing == true && e.Button == MouseButtons.Left)//确定是左键
{
Graphics g = Graphics.FromImage(mypicture);
//调出初始画布,这块画布是在鼠标移动的每一帧中不停叠加更新的
switch (mode)
{
case (Mode.BrushMode):
{
g.DrawLine(p, a.X, a.Y, e.X, e.Y); break;
//画短线模拟连续移动的过程,这是整个画图软件最精髓的一行代码
}
case (Mode.RectangleMode):
{
//将原来的矩形通过背景色覆盖掉之后再绘制画笔色的新矩形,以实现动画的效果,最终确定下来的矩形在MouseUp事件中被确定
//(存在将已作的图覆盖掉的bug)
g.DrawRectangle(new Pen(canvas, pensize), b.X, b.Y, a.X - b.X, a.Y - b.Y);
g.DrawRectangle(p, b.X, b.Y, e.X - b.X, e.Y - b.Y);
break;
}
case (Mode.OvalMode):
{
g.DrawEllipse(new Pen(canvas, pensize), b.X, b.Y, a.X - b.X, a.Y - b.Y);
g.DrawEllipse(p, b.X, b.Y, e.X - b.X, e.Y - b.Y);
break;
}
default:
break;
}
pictureBox1.Image = mypicture;//将新画出来的图在picturebox中显示
g.Dispose();//释放画笔资源
a.X = e.X;//更新点位置
a.Y = e.Y;
}
}
private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
//(无论什么模式)确定结束位置
Graphics g = Graphics.FromImage(mypicture);
switch (mode)
{
case (Mode.RectangleMode):
{
g.DrawRectangle(p, b.X, b.Y, a.X - b.X, a.Y - b.Y); break;
}
case (Mode.OvalMode):
{
g.DrawEllipse(p, b.X, b.Y, a.X - b.X, a.Y - b.Y); break;
}
default:
break;
}
pictureBox1.Image = mypicture;
g.Dispose();
startDrawing = false;
}
整个画图软件最精髓最核心的思想就是通过确定点的坐标,调用GDI+的各种函数来实现绘制各种图形。最简单的画笔功能就是在鼠标移动的过程中不断定位点,并在新点和上一个旧点之间绘制短线段来模拟连续的画笔移动过程。也就是这一行:g.DrawLine(p, a.X, a.Y, e.X, e.Y);
由于“画文字”的特殊性(鼠标一按下去不用拖动就要插入),故实现文字的功能被挪到了按下的事件里而非移动事件。在文字模式下按下鼠标,弹出一个含富文本框的窗口,在富文本框中可以输入自己的内容。点击确定之后,富文本框的text属性被传到全局变量中,之后通过DrawString函数绘制在画布上。
相比于市面上已有的画图板,自己开发的画图板还是存在不少漏洞,比如为了实现画矩形和椭圆的动态效果而对已有的画有擦除的bug。但是总的来说基本的要求还是实现了。同时还有许多功能没有完善,譬如更换背景图片,拉框选择图片的区域,放大缩小旋转等。这些较为复杂的功能将在日后不断完善的过程中实现。
通过本次实验,我们能了解到GDI+的基本用法、C#自带控件如SaveFileDialog, ColorDialog等的使用方法,进一步加深了C#图形化编程的技术。
李春葆,曾平,喻丹丹.C#程序设计教程(第3版):清华大学出版社,2015
汪维华,汪维清,胡章平.C#程序设计实用教程(第2版):清华大学出版社,2011
Copyright @ 2021, CSDN: ForeverMeteor, all rights reserved.