最近对
GDI+
这个东西接触的比较多,也做了些简单的实例,比如绘图板,仿
QQ
截图等.
最早接触这个类,是因为想做仿
QQ
截图的效果.巧的很,学会了如何做截图后,
.NET
课堂上老师也正巧要讲关于
c#
绘图方面的知识,并且我自己又在网上学习金老师的培训班,也是要用到这个类.在学习中有一些体会,所以准备把这些体会记下来,因为内容比较多,可能我会分几次写.
废话不多说了,我们先来认识一下这个
GDI+
,看看它到底长什么样
.
GDI+
:
Graphics Device Interface Plus
也就是图形设备接口
,
提供了各种丰富的图形图像处理功能
;
在
C#.NET
中,使用
GDI+
处理二维(
2D
)的图形和图像,使用
DirectX
处理三维(
3D
)的图形图像
,
图形图像处理用到的主要命名空间是
System . Drawing
:提供了对
GDI+
基本图形功能的访问,主要有
Graphics
类、
Bitmap
类、从
Brush
类继承的类、
Font
类、
Icon
类、
Image
类、
Pen
类、
Color
类等
.
大概了解了什么是
GDI+
后
,
我们来看一下绘图要用到的主要工具
,
要画图
,
肯定要画板
吧
,
在
C#
中画板可以通过
Graphics
这个类来创建
,
有了画板
,
总得弄个笔
什么之类的吧
,
不然怎么画呀
,
难不成我们用手指画
.
笔又可以分好多种类
,
比如铅笔
,
画刷等
.
它们的区别主要是铅笔可以用来画线条
,
而画刷呢
,
嘿嘿
,
自己考虑下
.
在
c#
中我们可以用
Pen,Brush
类来实现类似功能
.
颜料则自然是用
Color
类了
.
有了工具
,
我们就可以开始动手了
!(所需命名空间:using System.Drawing;
)
实现效果
:
在空白窗体中画基本图形
首先
准备一个画板
:
创建一个画板主要有
3
种方式
:
A:
在窗体或控件的
Paint
事件中直接引用
Graphics
对象
B:
利用窗体或某个控件的
CreateGraphics
方法
C:
从继承自图像的任何对象创建
Graphics
对象
这次我们就先以
A
为例说明问题
:

private
void
Form1_Paint(
object
sender, PaintEventArgs e)
{
Graphics g
=
e.Graphics;
//
创建画板,这里的画板是由Form提供的.
}

然后
,
我们要只笔
:

void
Form1_Paint(
object
sender, PaintEventArgs e)
{
Graphics g
=
e.Graphics;
//
创建画板,这里的画板是由Form提供的.
Pen p
=
new
Pen(Color.Blue,
2
);
//
定义了一个蓝色,宽度为的画笔
}

接下来
我们就可以来画画了
.

void
Form1_Paint(
object
sender, PaintEventArgs e)
{
Graphics g
=
e.Graphics;
//
创建画板,这里的画板是由Form提供的.
Pen p
=
new
Pen(Color.Blue,
2
);
//
定义了一个蓝色,宽度为的画笔
g.DrawLine(p,
10
,
10
,
100
,
100
);
//
在画板上画直线,起始坐标为(10,10),终点坐标为(100,100)
g.DrawRectangle(p,
10
,
10
,
100
,
100
);
//
在画板上画矩形,起始坐标为(10,10),宽为,高为
g.DrawEllipse(p,
10
,
10
,
100
,
100
);
//
在画板上画椭圆,起始坐标为(10,10),外接矩形的宽为,高为
}

1.
首先我们来看下上一片中我们使用过的
Pen.
Pen的属性主要有
: Color(颜色
),DashCap(短划线终点形状
),DashStyle(虚线样式
),EndCap(线尾形状
), StartCap(线头形状
),Width(粗细
)等
.
我们可以用
Pen 来画虚线
,带箭头的直线等

=
new
Pen(Color.Blue,
5
);
//
设置笔的粗细为,颜色为蓝色

Graphics g
=
this
.CreateGraphics();


//
画虚线

p.DashStyle
=
DashStyle.Dot;
//
定义虚线的样式为点

g.DrawLine(p,
10
,
10
,
200
,
10
);


//
自定义虚线
p.DashPattern
=
new
float
[]
{
2
,
1
}
;
//
设置短划线和空白部分的数组

g.DrawLine(p,
10
,
20
,
200
,
20
);


//
画箭头,只对不封闭曲线有用

p.DashStyle
=
DashStyle.Solid;
//
恢复实线

p.EndCap
=
LineCap.ArrowAnchor;
//
定义线尾的样式为箭头

g.DrawLine(p,
10
,
30
,
200
,
30
);




2.
接下来我们来看下
Brush
的使用
作用
:我们可以用画刷填充各种图形形状,如矩形、椭圆、扇形、多边形和封闭路径等
,主要有几种不同类型的画刷
:
•
SolidBrush:画刷最简单的形式,用纯色进行绘制
•
HatchBrush:类似于
SolidBrush,但是可以利用该类从大量预设的图案中选择绘制时要使用的图案,而不是纯色
•
TextureBrush:使用纹理(如图像)进行绘制
•
LinearGradientBrush:使用沿渐变混合的两种颜色进行绘制
•
PathGradientBrush :基于编程者定义的唯一路径,使用复杂的混合色渐变进行绘制
我们这里只是简单介绍使用其中的几种
:

=
this
.CreateGraphics();

=
new
Rectangle(
10
,
10
,
50
,
50
);
//
定义矩形,参数为起点横纵坐标以及其长和宽


//
单色填充

SolidBrush b1
=
new
SolidBrush(Color.Blue);
//
定义单色画刷

g.FillRectangle(b1, rect);
//
填充这个矩形


//
字符串

g.DrawString(
"
字符串
"
,
new
Font(
"
宋体
"
,
10
), b1,
new
PointF(
90
,
10
));


//
用图片填充

TextureBrush b2
=
new
TextureBrush(Image.FromFile(
@"
e:\picture\1.jpg
"
));

=
new
Point(
10
,
70
);
//
更改这个矩形的起点坐标

rect.Width
=
200
;
//
更改这个矩形的宽来

rect.Height
=
200
;
//
更改这个矩形的高

g.FillRectangle(b2, rect);


//
用渐变色填充

rect.Location
=
new
Point(
10
,
290
);

=
new
LinearGradientBrush(rect, Color.Yellow , Color.Black , LinearGradientMode.Horizontal);


运行效果图
:
3.
坐标轴变换
在
winform中的坐标轴和我们平时接触的平面直角坐标轴不同
,winform中的坐标轴方向完全相反
:窗体的左上角为原点
(0,0),水平向左则
X增大
,垂直下向则
Y增大
运行效果图:
4.
最后我们来看下
Graphics
这个画板上我们还可以画什么
其实我们上面用到的都是在画一些简单的图形
,直线
,矩形
,扇形
,圆孤等
,我们还可以用它来绘制图片
,这可以用它的
DrawImage方法
.
这里我不详细讲解
,大家有兴趣可以自己去
MSDN了解下
.
我们后面会讲到的截图就会用到这个方法
接下来
,
我们来实际操作下
,
通过旋转坐标轴的方向来画出不同角度的图案
,
或通过更改坐标原点的位置来平衡坐标轴的位置

=
this
.CreateGraphics();


//
单色填充

//
SolidBrush b1 = new SolidBrush(Color.Blue);
//
定义单色画刷

Pen p
=
new
Pen(Color.Blue,
1
);


//
转变坐标轴角度

for
(
int
i
=
0
; i
<
90
; i
++
)
{
g.RotateTransform(i);
//
每旋转一度就画一条线
g.DrawLine(p,
0
,
0
,
100
,
0
);
g.ResetTransform();
//
恢复坐标轴坐标
}


//
平移坐标轴

g.TranslateTransform(
100
,
100
);

0
,
0
,
100
,
0
);



//
先平移到指定坐标,然后进行度旋转

g.TranslateTransform(
100
,
200
);

for
(
int
i
=
0
; i
<
8
; i
++
)
{
g.RotateTransform(
45
);
g.DrawLine(p,
0
,
0
,
100
,
0
);
}


我们先来做一个简单的----仿QQ截图,关于这个的例子其实网上已经有这方面的资料了,但是为了文章的完整性,还是觉得有必要讲解.
我们先来看一下效果:
(图1)
(图2)
接下来看看这是如何做到的.
思路:聊天窗体上有一个截图按钮,点击按钮后,程序将整个屏幕画在一个新的全屏窗体上,然后显示这个窗体.因为是全屏的窗体,并且隐藏了菜单栏、工具栏等,所以在我们看来就好像是一个桌面的截图,然后在这个新窗体上画矩形,最后保存矩形中的内容并显示在原来的聊天窗体中.
步骤:
A.新建一个窗体.命名为Catch.然后设置这个窗体的FormBorderStyle为None,WindowState为Maximized.
B.我们对代码进行编辑:
C.创建了Catch窗体后,我们在截图按钮(位于聊天窗体上)上加入以下事件:

private
void
bCatch_Click(
object
sender, EventArgs e)
{
if
(bCatch_HideCurrent.Checked)
{
this
.Hide();
//
隐藏当前窗体
Thread.Sleep(
50
);
//
让线程睡眠一段时间,窗体消失需要一点时间
Catch CatchForm
=
new
Catch();
Bitmap CatchBmp
=
new
Bitmap(Screen.AllScreens[
0
].Bounds.Width, Screen.AllScreens[
0
].Bounds.Height);
//
新建一个和屏幕大小相同的图片
Graphics g
=
Graphics.FromImage(CatchBmp);
g.CopyFromScreen(
new
Point(
0
,
0
),
new
Point(
0
,
0
),
new
Size(Screen.AllScreens[
0
].Bounds.Width, Screen.AllScreens[
0
].Bounds.Height));
//
保存全屏图片
CatchForm.BackgroundImage
=
CatchBmp;
//
将Catch窗体的背景设为全屏时的图片
if
(CatchForm.ShowDialog()
==
DialogResult.OK)
{
//
如果Catch窗体结束,就将剪贴板中的图片放到信息发送框中
IDataObject iData
=
Clipboard.GetDataObject();
DataFormats.Format myFormat
=
DataFormats.GetFormat(DataFormats.Bitmap);
if
(iData.GetDataPresent(DataFormats.Bitmap))
{
richtextbox1.Paste(myFormat);
Clipboard.Clear();
//
清除剪贴板中的对象
}
this
.Show();
//
重新显示窗体
}
}
}
这样我们的截图功能便完成了.
我想对于初学者来说如何消去第一次绘制的图片是个比较困难的问题.如果没有采取措施,你会发现只要你鼠标移动,就会画一个矩形,这样便会出现N多的矩形,而我们只是要最后的那一个.
一般解决这种问题的方法有两种:
1.就是在绘制第二个图形时,我们先用与底色相同的颜色将上次绘制的图形重新绘制一下.但这往往需要底色为纯色时使用.
2.我们并不直接将图形画在画板上,我们用一个图片A来保存原画板上的图片.然后再新建一个与图片A相同的图片B,将我们要绘制的图形画在该图片B上,然后再将该图片B画在画板上.这样图片A并没有被改变.于是第二次画的时候我们还是同样新建一个与图片A相同的图片进行绘制.那么上一次的图形就不会被保留下来.问题也就解决了.
下一次,向大家介绍如何做一个仿windows画板的程序
个人认为如果想做一个功能强大的绘图工具,那么单纯掌握GDI还远远不够,我的目前也只能做一个比较简单的绘图工具了.不足之处,欢迎大家讨论!
先来看一下最终效果吧:

主要实现功能:画直线,矩形,橡皮,圆形,切换颜色,打开图片,保存图片,清除图片,手动调节画布大小;软件刚启动时,为一张空白画布,我们可以直接在画布上绘画,也可以通过菜单中的“打开”,导入一张图片,然后我们就可以在这张图片上进行绘制。
平台:VS2005 WINFORM
由于代码过多,在这里只简要介绍下制作步骤,提供大家工程下载.
1.对整个界面进行布局.
2.实现绘图工具的功能
3.实现颜色拾取的功能,这里我们直接拿上次写的自定义控件来用.
4.实现菜单功能
5.实现手动调节画布大小的功能
6.测试
实现绘图工具的功能
为了让代码藕合度小点,稍许用了些设计模式,因为不是很会,所以代码还是有点乱乱的,嘿嘿!关于绘图工具的这些功能块全部写在了DrawTools这个类里.那么在主窗体中,只需要调用这个类来完成绘制就行了,而不需要过多的涉及到具体的绘图代码。绘图工具这个类提供的主要工具就是:铅笔、橡皮、直线、矩形、圆形、实心矩形、实心圆形。关于这些功能块的代码,并不难,只要大家对认真看过前几篇内容,那应该都看得懂。
这里要注意以下几点:
1.如何防止记录不必要的绘图过程中的痕迹?
这个问题在第三篇
中有提到过,大家不妨先去看看那一篇。为了让代码看起来可读性高点,我设置了两个Image变量,finishingImg用来保存绘图过程中的痕迹,orginalImg用来保存已完成的绘图过程和初始时的背景图片。
2.这个类如何与主窗体进行通信?
当然如果直接将这些功能块写在主窗体中自然没有这个问题。但是那样代码会显得很混杂,如果只是工具代码出现问题就需要改整个项目。我在这里通过定义方法和属性,让主窗体通过给属性赋值将画板画布以及颜色什么的信息传给这个工具类,然后通过调用相应的工具方法来使用这些工具。
3.关键属性
要想让这些工具能正常使用,必须传递给他以下几样东西:目标画板(也就是picturebox),绘图颜色,原始画布。
实现菜单功能
这里就需要我们对文件的操作有一点了解,大家可以去查一下相关资料。
难点主要就是“打开”这个菜单项的实现
我们要实现将打开后的图片在修改后重新保存就必须让文件在打开后就能关闭,否则就会因为文件打开而无法覆盖原文件。就会导致编译时弹出“GDI 一般性错误”。所以根据网上其它朋友的做法就是先将打开的图片通过GDI+将图片画到另一个画布上,然后及时关闭打开的图片和用来绘制该图片的画板。详见http://www.wanxin.org/redirect.php?tid=3&goto=lastpost

private
void
openPic_Click(
object
sender, EventArgs e)
{
OpenFileDialog ofd
=
new
OpenFileDialog();
//
实例化文件打开对话框
ofd.Filter
=
"
JPG|*.jpg|Bmp|*.bmp|所有文件|*.*
"
;
//
设置对话框打开文件的括展名
if
(ofd.ShowDialog()
==
DialogResult.OK)
{
Bitmap bmpformfile
=
new
Bitmap(ofd.FileName);
//
获取打开的文件
panel2.AutoScrollPosition
=
new
Point(
0
,
0
);
//
将滚动条复位
pbImg.Size
=
bmpformfile.Size;
//
调整绘图区大小为图片大小
reSize.Location
=
new
Point(bmpformfile.Width, bmpformfile.Height);
//
reSize为我用来实现手动调节画布大小用的
//
因为我们初始时的空白画布大小有限,"打开"操作可能引起画板大小改变,所以要将画板重新传入工具类
dt.DrawTools_Graphics
=
pbImg.CreateGraphics();
Bitmap bmp
=
new
Bitmap(pbImg.Width, pbImg.Height);
Graphics g
=
Graphics.FromImage(bmp);
g.FillRectangle(
new
SolidBrush(pbImg.BackColor),
new
Rectangle(
0
,
0
, pbImg.Width, pbImg.Height));
//
不使用这句话,那么这个bmp的背景就是透明的
g.DrawImage(bmpformfile,
0
,
0
,bmpformfile.Width,bmpformfile.Height);
//
将图片画到画板上
g.Dispose();
//
释放画板所占资源
//
不直接使用pbImg.Image = Image.FormFile(ofd.FileName)是因为这样会让图片一直处于打开状态,也就无法保存修改后的图片
bmpformfile.Dispose();
//
释放图片所占资源
g
=
pbImg.CreateGraphics();
g.DrawImage(bmp,
0
,
0
);
g.Dispose();
dt.OrginalImg
=
bmp;
bmp.Dispose();
sFileName
=
ofd.FileName;
//
储存打开的图片文件的详细路径,用来稍后能覆盖这个文件
ofd.Dispose();
}
}
清除图像其实就是用白色填充整个画布
其它的都比较简单,这就不具体讲了。
实现手动调节画布大小
网上有人说使用API,但是个人觉得还是使用其它控件帮忙比较简单,至少我们还看得懂。
思路:放置一个picturebox1(尺寸为5*5),将它固定在主画板的右下角,然后改变鼠标进入时的Cursor为箭头形状,设置鼠标按下移动时的事件,让该picturebox1 跟随鼠标移动。当鼠标松开时,将主画板的右下角坐标调整为picturebox1的坐标。
下面来看下代码:
其中的reSize就是我们用来帮忙的picturebox控件

private
bool
bReSize
=
false
;
//
是否改变画布大小

private
void
reSize_MouseDown(
object
sender, MouseEventArgs e)
{
bReSize
=
true
;
//
当鼠标按下时,说明要开始调节大小
}


private
void
reSize_MouseMove(
object
sender, MouseEventArgs e)
{
if
(bReSize)
{
reSize.Location
=
new
Point(reSize.Location.X
+
e.X, reSize.Location.Y
+
e.Y);
}
}


private
void
reSize_MouseUp(
object
sender, MouseEventArgs e)
{
bReSize
=
false
;
//
大小改变结束
//
调节大小可能造成画板大小超过屏幕区域,所以事先要设置autoScroll为true.
//
但是滚动条的出现反而增加了我们的难度,因为滚动条上下移动并不会自动帮我们调整图片的坐标。
//
这是因为GDI绘图的坐标系不只一个,好像有三个,没有仔细了解,一个是屏幕坐标,一个是客户区坐标,还个是文档坐标。
//
滚动条的上下移动改变的是文档的坐标,但是客户区坐标不变,而location属性就属于客户区坐标,所以我们直接计算会出现错误
//
这时我们就需要知道文档坐标与客户区坐标的偏移量,这就是AutoScrollPostion可以提供的
pbImg.Size
=
new
Size(reSize.Location.X
-
(
this
.panel2.AutoScrollPosition.X), reSize.Location.Y
-
(
this
.panel2.AutoScrollPosition.Y));
dt.DrawTools_Graphics
=
pbImg.CreateGraphics();
//
因为画板的大小被改变所以必须重新赋值
//
另外画布也被改变所以也要重新赋值
Bitmap bmp
=
new
Bitmap(pbImg.Width, pbImg.Height);
Graphics g
=
Graphics.FromImage(bmp);
g.FillRectangle(
new
SolidBrush(Color.White),
0
,
0
, pbImg.Width, pbImg.Height);
g.DrawImage(dt.OrginalImg,
0
,
0
);
g.Dispose();
g
=
pbImg.CreateGraphics();
g.DrawImage(bmp,
0
,
0
);
g.Dispose();
dt.OrginalImg
=
bmp;
bmp.Dispose();
}