最近對GDI+這個東西接觸的比較多,也做了些簡單的實例,比如繪圖板,仿QQ截圖等.
廢話不多說了,我們先來認識一下這個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提供的.
}
然后,我們要只筆:
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics; //創建畫板,這里的畫板是由Form提供的.
Pen p = new Pen(Color.Blue, 2);//定義了一個藍色,寬度為的畫筆
}
接下來我們就可以來畫畫了.
private 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),外接矩形的寬為,高為
}
效果圖如下:
在上一片里已經向大家介紹了如何使用GDI+繪制簡單的圖像,這一片繼續向大家介紹其它一些繪圖知識.
1.首先我們來看下上一片中我們使用過的Pen.
Pen的屬性主要有: Color(顏色),DashCap(短划線終點形狀),DashStyle(虛線樣式),EndCap(線尾形狀), StartCap(線頭形狀),Width(粗細)等.我們可以用Pen 來畫虛線,帶箭頭的直線等
Pen p = 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);
g.Dispose();
p.Dispose();
以上代碼運行結果:
2.接下來我們來看下Brush的使用
作用:我們可以用畫刷填充各種圖形形狀,如矩形、橢圓、扇形、多邊形和封閉路徑等,主要有幾種不同類型的畫刷:
? SolidBrush:畫刷最簡單的形式,用純色進行繪制
? HatchBrush:類似於 SolidBrush,但是可以利用該類從大量預設的圖案中選擇繪制時要使用的圖案,而不是純色
? TextureBrush:使用紋理(如圖像)進行繪制
? LinearGradientBrush:使用沿漸變混合的兩種顏色進行繪制
? PathGradientBrush :基於編程者定義的唯一路徑,使用復雜的混合色漸變進行繪制
我們這里只是簡單介紹使用其中的幾種:
Graphics g = this.CreateGraphics();
Rectangle rect = 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:picture1.jpg"));
rect.Location = new Point(10, 70);//更改這個矩形的起點坐標
rect.Width = 200;//更改這個矩形的寬來
rect.Height = 200;//更改這個矩形的高
g.FillRectangle(b2, rect);
//用漸變色填充
rect.Location = new Point(10, 290);
LinearGradientBrush b3 = new LinearGradientBrush(rect, Color.Yellow , Color.Black , LinearGradientMode.Horizontal);
g.FillRectangle(b3, rect);
運行效果圖:
3.坐標軸變換
在winform中的坐標軸和我們平時接觸的平面直角坐標軸不同,winform中的坐標軸方向完全相反:窗體的左上角為原點(0,0),水平向左則X增大,垂直下向則Y增大
接下來,我們來實際操作下,通過旋轉坐標軸的方向來畫出不同角度的圖案,或通過更改坐標原點的位置來平衡坐標軸的位置.
Graphics g = 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);
g.DrawLine(p, 0, 0, 100, 0);
g.ResetTransform();
//先平移到指定坐標,然后進行度旋轉
g.TranslateTransform(100,200);
for (int i = 0; i < 8; i++)
{
g.RotateTransform(45);
g.DrawLine(p, 0, 0, 100, 0);
}
g.Dispose();
運行效果圖:
4.最后我們來看下Graphics這個畫板上我們還可以畫什么
其實我們上面用到的都是在畫一些簡單的圖形,直線,矩形,扇形,圓孤等,我們還可以用它來繪制圖片,這可以用它的DrawImage方法.這里我不詳細講解,大家有興趣可以自己去MSDN了解下.我們后面會講到的截圖就會用到這個方法.
感謝大家的支持,這幾天從早忙到晚,一個字累呀!!!現在挺困的,但是又不習慣這么早睡覺,哎~~還是利用這個時間繼續來寫第三篇吧.
前兩篇已經基本向大家介紹了繪圖的基本知識.那么,我就用我們上兩篇所學的,做幾個例子.
我們先來做一個簡單的----仿QQ截圖關於這個的例子其實網上已經有這方面的資料了,但是為了文章的完整性,還是覺得有必要講解.
我們先來看一下效果:
(圖1)
(圖2)
接下來看看這是如何做到的.
思路:聊天窗體上有一個截圖按鈕,點擊按鈕后,程序將整個屏幕畫在一個新的全屏窗體上,然后顯示這個窗體.因為是全屏的窗體,並且隱藏了菜單欄、工具欄等,所以在我們看來就好像是一個桌面的截圖,然后在這個新窗體上畫矩形,最后保存矩形中的內容並顯示在原來的聊天窗體中.
步驟:
A.新建一個窗體.命名為Catch.然后設置這個窗體的FormBorderStyle為None,WindowState為Maximized.
B.我們對代碼進行編輯:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace Client
{
public partial class Catch : Form
{
public Catch()
{
InitializeComponent();
}
用戶變量#region 用戶變量
private Point DownPoint = Point.Empty;//記錄鼠標按下坐標,用來確定繪圖起點
private bool CatchFinished = false;//用來表示是否截圖完成
private bool CatchStart = false;//表示截圖開始
private Bitmap originBmp;//用來保存原始圖像
private Rectangle CatchRect;//用來保存截圖的矩形
#endregion
//窗體初始化操作
private void Catch_Load(object sender, EventArgs e)
{
this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true);
this.UpdateStyles();
//以上兩句是為了設置控件樣式為雙緩沖,這可以有效減少圖片閃爍的問題,關於這個大家可以自己去搜索下
originBmp = new Bitmap(this.BackgroundImage);//BackgroundImage為全屏圖片,我們另用變量來保存全屏圖片
}
//鼠標右鍵點擊結束截圖
private void Catch_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
this.DialogResult = DialogResult.OK;
this.Close();
}
}
//鼠標左鍵按下時動作
private void Catch_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
if (!CatchStart)
{//如果捕捉沒有開始
CatchStart = true;
DownPoint = new Point(e.X, e.Y);//保存鼠標按下坐標
}
}
}
private void Catch_MouseMove(object sender, MouseEventArgs e)
{
if (CatchStart)
{//如果捕捉開始
Bitmap destBmp = (Bitmap)originBmp.Clone();//新建一個圖片對象,並讓它與原始圖片相同
Point newPoint = new Point(DownPoint.X, DownPoint.Y);//獲取鼠標的坐標
Graphics g = Graphics.FromImage(destBmp);//在剛才新建的圖片上新建一個畫板
Pen p = new Pen(Color.Blue,1);
int width = Math.Abs(e.X - DownPoint.X), height = Math.Abs(e.Y - DownPoint.Y);//獲取矩形的長和寬
if (e.X < DownPoint.X)
{
newPoint.X = e.X;
}
if (e.Y < DownPoint.Y)
{
newPoint.Y = e.Y;
}
CatchRect = new Rectangle(newPoint,new Size(width,height));//保存矩形
g.DrawRectangle(p,CatchRect);//將矩形畫在這個畫板上
g.Dispose();//釋放目前的這個畫板
p.Dispose();
Graphics g1 = this.CreateGraphics();//重新新建一個Graphics類
//如果之前那個畫板不釋放,而直接g=this.CreateGraphics()這樣的話無法釋放掉第一次創建的g,因為只是把地址轉到新的g了.如同string一樣
g1 = this.CreateGraphics();//在整個全屏窗體上新建畫板
g1.DrawImage(destBmp,new Point(0,0));//將剛才所畫的圖片畫到這個窗體上
//這個也可以屬於二次緩沖技術,如果直接將矩形畫在窗體上,會造成圖片抖動並且會有無數個矩形.
g1.Dispose();
destBmp.Dispose();//要及時釋放,不然內存將會被大量消耗
}
}
private void Catch_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
if (CatchStart)
{
CatchStart = false;
CatchFinished = true;
}
}
}
//鼠標雙擊事件,如果鼠標位於矩形內,則將矩形內的圖片保存到剪貼板中
private void Catch_MouseDoubleClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left&&CatchFinished)
{
if (CatchRect.Contains(new Point(e.X, e.Y)))
{
Bitmap CatchedBmp = new Bitmap(CatchRect.Width, CatchRect.Height);//新建一個於矩形等大的空白圖片
Graphics g = Graphics.FromImage(CatchedBmp);
g.DrawImage(originBmp, new Rectangle(0, 0, CatchRect.Width, CatchRect.Height), CatchRect, GraphicsUnit.Pixel);
//把orginBmp中的指定部分按照指定大小畫在畫板上
Clipboard.SetImage(CatchedBmp);//將圖片保存到剪貼板
g.Dispose();
CatchFinished = false;
this.BackgroundImage = originBmp;
CatchedBmp.Dispose();
this.DialogResult = DialogResult.OK;
this.Close();
}
}
}
}
}
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+來繪圖,並做了一個截圖的實例,這篇我向大家介紹下如何來做一個類似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();
}
效果如下圖(仔細看白色區域的右下角):
此時就可以通過拖動那個小方塊來調節圖片大小了。
這樣,主要的問題差不多已經解決了,但還是有不足這處,歡迎大家提出寶貴的意見。