//2020年6月17 更新
這篇博客最近好像挺火?不少師弟師妹咨詢我MFC的內容,額,博主很久也沒有用MFC搞事情了,現在甚至都沒有開發環境,加上到期末了,事情很多,實在沒有精力回顧幫大家解決問題,今年這種情況特殊,我也很能理解18級的師弟師妹們,教授實訓課的應該還是張帆老師吧,很好的老師,可以多向他請教,B站好像也有相關的教程,方法總比困難多,加油!
一、背景
喔,五天的實訓終於結束了,學校安排的這次實訓課名稱叫高級程序設計實訓,但在我看來,主要是學習了Visual C++ .NET所提供的MFC(Microsoft Foundation Class)庫所提供的類及其功能函數的使用。寫這一篇博客的目的是針對實訓中出現的問題做一些說明,方便以后查看,並且對這次實訓做一些總結。這一次的實訓對我來說其實挺難受的,真正用來學習使用VS和MFC的時間只有三天,加上下個周是考試周,還有幾門課沒有復習完,這幾天基本上是連軸轉,中午也泡在實驗室里,唉啊還是自己太菜了。最后我們需要提交一個課程設計程序,因為時間的原因,我選擇了最簡單的圖形界面編輯工具,這個程序其實在C++的課程設計上就有這個,但當時我還不會windows圖形界面的編程,現在想想這兩個課程設計其實完全可以是一份(捂臉)。
最后做出來的界面是這樣的:
在功能上:
- 能夠在windows的界面下畫圖,能夠畫直線、空心矩形、、圓角矩形、空心圓形、填充矩形、填充圓形、填充圓角矩形和文字。
- 能夠改變畫圖是畫筆用的顏色、線寬、線型和填充用的顏色、字體。
- 能夠保存、打開所做的圖形文件
- 擁有菜單、工具欄、鼠標右鍵等編輯界面。
二、程序說明
1.工具欄說明
2.畫圖菜單
在畫圖菜單下,能夠選擇畫直線、空心矩形、空心圓形、空心圓角矩形、填充矩形、填充圓形、填充圓角矩形。
3.文本菜單
文本輸入菜單下有兩個選項一個是文本輸入、一個是字體設置,分別對應着兩個對話框。
文本輸入對話框,能夠根據指定的x、y橫縱坐標來定位輸入位置,打印輸入的相應信息。
而字體設置,調用系統自帶的對話框,完成對字體類型、字形和字體大小的設置。
4.畫筆設置菜單
畫筆設置菜單下有畫筆顏色、畫筆類型、畫筆寬度三個選項。其中畫筆類型又包含實線、虛線、點線、點划線、雙點划線五個選項。畫筆類型根據查閱課本內容和上網搜索得知,只有在寬度為1的時候,才能顯示除實線外的其他畫筆類型,當寬度大於1時畫出來的都是實線類型的線條。
顏色設置,調用系統自帶的對話框,完成對畫筆、畫刷顏色的選擇。同時選用該對話框能夠實現自定義顏色。
畫筆寬度設置對話框是自己設置的對話框,輸入相應的畫筆寬度,實現畫筆寬度的改變。
5.界面下鼠標右鍵
右擊鼠標會有鼠標右鍵菜單,其功能選項與功能欄所給的功能是一樣的,選擇畫直線、空心矩形、空心圓形、空心圓角矩形、填充矩形、填充圓形、填充圓角矩形和文本。
三、鼠標拖動繪畫
該程序的基礎功能就是能夠拖動鼠標來繪制圖形,這里面實際上用到的是橡皮筋技術。在鼠標拖動中,每當鼠標的位置發生了改變,需要清除已經繪制的線段,課本已經該出了實現該過程的代碼。當然之前需要在視圖View類中添加鼠標左鍵按下,鼠標移動,鼠標左鍵抬起的消息映射。
void CShirrView::OnLButtonDown(UINT nFlags, CPoint point) { //將鼠標左鍵按下位置存儲到p1、p2 p1 = p2 = point; b=true; //設置繪圖標志 pdc->SetROP2(R2_NOTXORPEN);//設置繪圖模式為R2_NOTXORPEN,注意背景為白色 CView::OnLButtonDown(nFlags, point); } void CShirrView::OnMouseMove(UINT nFlags, CPoint point) { if (!b) return; //如果不是繪圖狀態,返回 //P1為鼠標左鍵按下位置,P2為鼠標上次位置 //即按前次位置重繪了一次,模式是R2_NOTXORPEN //最終效果是白色,由於底色為白,實際效果是清除了上次的線段 pdc->MoveTo(p1.x,p1.y); pdc->LineTo(p2.x,p2.y); p2 = point; //p1仍為鼠標左鍵按下位置,P2為當前鼠標位置 pdc->MoveTo(p1.x,p1.y); pdc->LineTo(p2.x,p2.y); //從P1到鼠標當前位置繪制線段 CView::OnMouseMove(nFlags, point); } void CShirrView::OnLButtonDown(UINT nFlags, CPoint point) { //將鼠標左鍵按下位置存儲到p1、p2 p1 = p2 = point; b=true; //設置繪圖標志 pdc->SetROP2(R2_NOTXORPEN);//設置繪圖模式為R2_NOTXORPEN,注意背景為白色 CView::OnLButtonDown(nFlags, point); } void CShirrView::OnMouseMove(UINT nFlags, CPoint point) { if (!b) return; //如果不是繪圖狀態,返回 //P1為鼠標左鍵按下位置,P2為鼠標上次位置 //即按前次位置重繪了一次,模式是R2_NOTXORPEN //最終效果是白色,由於底色為白,實際效果是清除了上次的線段 pdc->MoveTo(p1.x,p1.y); pdc->LineTo(p2.x,p2.y); p2 = point; //p1仍為鼠標左鍵按下位置,P2為當前鼠標位置 pdc->MoveTo(p1.x,p1.y); pdc->LineTo(p2.x,p2.y); //從P1到鼠標當前位置繪制線段 CView::OnMouseMove(nFlags, point); }
上面的代碼是用來畫直線的,能夠完成畫直線的功能,那么就可以照貓畫虎實現畫矩形、畫圓的功能了,這些圖形都需要起點和終點的坐標作為畫圖的參數。
同時我們要明白鼠標相應這些函數是在當前視圖中執行的,也就是說,我們一打開該程序,只要在視圖中點擊移動鼠標,這些函數其實都會相應執行到,那么我們該怎么去設計選擇不同的圖形?
其實這很簡單,改造鼠標移動消息相應函數和鼠標左鍵抬起消息響應函數即可!我們可以給不同的圖形一個編號,按下選擇圖形的按鈕后,相對應的消息相應函數就會改變那個編號,鼠標移動消息相應函數和鼠標左鍵抬起消息響應函數根據這個編號來繪制不同的圖形就可以了!
那鼠標左鍵按下消息響應函數不用去改造嗎?
是不用改造的,因為鼠標一開始按下只是為了獲取起點的坐標,而是不去畫圖形,所以這個對所有的圖形都適用。
在這之前需要記錄好每一個選擇圖形按鍵的ID,和消息響應函數,同時在消息響應函數中完成了CDC對象指針pdc的構造。
/* 1 畫直線 2 畫矩形 3.畫空心圓形 4.畫填充矩形 5.畫填充圓形 6.畫圓角矩形 7.畫填充圓角矩形 直線 ID_LINE, 矩形 ID_RECTANGLE 圓形 ID_CIRCLE 填充矩形 ID_TRECTANGLE 填充圓形 ID_TCIRCLE 圓角矩形 ID_YTRECTANGLE 填充圓角矩形 ID_TYTRECTANGLE */ void CWkfDrawingView::OnLine() { // TODO: 在此添加命令處理程序代碼 MyDrawStyle = 1; pdc=new CClientDC(this);//構造對象 b=false; } void CWkfDrawingView::OnRectangle()//畫矩形 { MyDrawStyle = 2; pdc=new CClientDC(this);//構造對象 b=false; } void CWkfDrawingView::OnCircle()//畫空心圓形 { MyDrawStyle = 3; pdc=new CClientDC(this);//構造對象 b=false; } void CWkfDrawingView::OnTrectangle() { MyDrawStyle = 4; pdc=new CClientDC(this);//構造對象 b=false; } void CWkfDrawingView::OnTcircle() { MyDrawStyle = 5; pdc=new CClientDC(this);//構造對象 b=false; } void CWkfDrawingView::OnYtrectangle() { MyDrawStyle = 6; pdc=new CClientDC(this);//構造對象 b=false; } void CWkfDrawingView::OnTytrectangle() { MyDrawStyle = 7; pdc=new CClientDC(this);//構造對象 b=false; }
下面給出鼠標按下消息相應函數、鼠標移動消息相應函數和鼠標左鍵抬起消息響應函數的代碼,MyStart 和MyEnd是視圖類的兩個CPoint類型的成員變量,用來保存起點和終點的坐標。
void CWkfDrawingView::OnLButtonDown(UINT nFlags, CPoint point)//鼠標按下 { // TODO: 在此添加消息處理程序代碼和/或調用默認值 MyStart = MyEnd = point; pdc=new CClientDC(this); pdc->SetROP2(R2_NOTXORPEN); b = true; CView::OnLButtonDown(nFlags, point); } void CWkfDrawingView::OnMouseMove(UINT nFlags, CPoint point)//鼠標移動 { // TODO: 在此添加消息處理程序代碼和/或調用默認值 /*pdc->MoveTo(MyStart.x,MyStart.y); pdc->LineTo(MyEnd.x,MyEnd.y);*/ if(!b) return ; CPen pen(GP.type, GP.width, GP.pencolor); OldPen=pdc->SelectObject(&pen); if(MyDrawStyle==1) { pdc->SelectStockObject(NULL_BRUSH); pdc->MoveTo(MyStart.x,MyStart.y); pdc->LineTo(MyEnd.x,MyEnd.y); MyEnd=point; pdc->MoveTo(MyStart.x,MyStart.y); pdc->LineTo(MyEnd.x,MyEnd.y); } else if(MyDrawStyle==2) { pdc->SelectStockObject(NULL_BRUSH); pdc->Rectangle(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); MyEnd = point; pdc->Rectangle(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); } else if(MyDrawStyle==3) { pdc->SelectStockObject(NULL_BRUSH); pdc->Ellipse(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); MyEnd = point; pdc->Ellipse(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); } else if(MyDrawStyle==4) { //pdc->SelectObject(&newBrush); CBrush bsh; bsh.CreateSolidBrush(GP.pencolor); pdc->SelectObject(&bsh); pdc->Rectangle(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); MyEnd = point; pdc->Rectangle(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); bsh.DeleteObject(); } else if(MyDrawStyle==5) { //pdc->SelectObject(&newBrush); CBrush bsh; bsh.CreateSolidBrush(GP.pencolor); pdc->SelectObject(&bsh); pdc->Ellipse(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); MyEnd = point; pdc->Ellipse(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); bsh.DeleteObject(); } else if(MyDrawStyle==6) { pdc->SelectStockObject(NULL_BRUSH); pdc->RoundRect(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y,a.x,a.y); MyEnd = point; pdc->RoundRect(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y,a.x,a.y); } else if(MyDrawStyle==7) { CBrush bsh; bsh.CreateSolidBrush(GP.pencolor); pdc->SelectObject(&bsh); pdc->RoundRect(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y,a.x,a.y); MyEnd = point; pdc->RoundRect(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y,a.x,a.y); bsh.DeleteObject(); } CView::OnMouseMove(nFlags, point); } void CWkfDrawingView::OnLButtonUp(UINT nFlags, CPoint point)//鼠標抬起 { GPen g; g.start = MyStart; g.end = MyEnd; g.width = MyWidth; g.type = type; g.style = MyDrawStyle; g.pencolor = GP.pencolor; if(MyDrawStyle==1) { // TODO: 在此添加消息處理程序代碼和/或調用默認值 pdc->SetROP2(R2_COPYPEN);//當前顏色覆蓋背景顏色 pdc->MoveTo(MyStart.x,MyStart.y); pdc->LineTo(point.x,point.y); g.c = GP.pencolor; b=false;//解除繪圖關系 CView::OnLButtonUp(nFlags, point); } else if(MyDrawStyle==2) { pdc->SetROP2(R2_COPYPEN); pdc->Rectangle(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); g.c = GP.pencolor; b=false;//解除繪圖關系 CView::OnLButtonUp(nFlags, point); } else if(MyDrawStyle==3) { pdc->SetROP2(R2_COPYPEN); pdc->Ellipse(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); g.c = GP.pencolor; b=false;//解除繪圖關系 CView::OnLButtonUp(nFlags, point); } else if(MyDrawStyle==4) { //pdc->SelectObject(&newBrush); CBrush bsh; bsh.CreateSolidBrush(GP.pencolor); pdc->SetROP2(R2_COPYPEN); pdc->SelectObject(&bsh); pdc->Rectangle(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); g.c =GP.pencolor; b=false;//解除繪圖關系 CView::OnLButtonUp(nFlags, point); } else if(MyDrawStyle==5) { //pdc->SelectObject(&newBrush); CBrush bsh; bsh.CreateSolidBrush(GP.pencolor); pdc->SetROP2(R2_COPYPEN); pdc->SelectObject(&bsh); pdc->Ellipse(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); g.c =GP.pencolor; b=false;//解除繪圖關系 CView::OnLButtonUp(nFlags, point); } else if(MyDrawStyle==6) { pdc->SetROP2(R2_COPYPEN); pdc->RoundRect(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y,a.x,a.y); g.angle=a; g.c = GP.pencolor; b=false;//解除繪圖關系 CView::OnLButtonUp(nFlags, point); } else if(MyDrawStyle==7) { //pdc->SelectObject(&newBrush); CBrush bsh; bsh.CreateSolidBrush(GP.pencolor); pdc->SetROP2(R2_COPYPEN); pdc->SelectObject(&bsh); pdc->RoundRect(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y,a.x,a.y); g.angle=a; g.c = GP.pencolor; b=false;//解除繪圖關系 CView::OnLButtonUp(nFlags, point); } GetDocument()->Mylist.AddTail(g);//保存信息 Invalidate(); }
上面的代碼中也嵌入了畫筆和畫刷的內容,畫筆和畫刷有一個特性就是一旦被定義和創建后,之后所繪制的圖形就會之間用上了,所以要注意畫筆和畫刷的使用,而填充圖形和空心圖形的區別就是有沒有畫刷。
CBrush bsh;//定義畫刷 bsh.CreateSolidBrush(GP.pencolor);//創建畫刷 pdc->SelectObject(&bsh);//選擇畫刷
畫筆的使用
CPen pen(GP.type, GP.width, GP.pencolor);//畫筆的定義和創建,三個參數畫筆的類型、畫筆寬度、畫筆的顏色 pdc->SelectObject(&pen);//選擇畫筆
上面還有一些代碼是關於圖形保存和重繪的,之后進行說明。
四、文件保存和讀取——文檔串行化
文檔類中中提供了文檔串行化(Serialize)函數能夠將對象當前的狀態由成員變量的值表示寫入硬盤中,下次再從硬盤中讀取對象的狀態,從而重建對象。
但在這里的對象是什么呢?
是圖形,可是圖形的種類很多,如果將每一個圖形的信息都通過結構體定義出來,那么會有很多的結構體來表示不同的圖形,這里我選擇了一種方法,將所有圖形的參數,不管是特有的參數還是共有的參數,都統統封裝到一個結構體中,為這個結構體創建鏈表,修改串行化函數就可以了!
//為了讓視圖和文檔都認識GPen這個存儲圖片信息的結構體,需要在Stdafx.h中添加代碼 struct GPen//保存畫筆參數全局變量 { int type;//畫筆類型 int width;//畫筆寬度 COLORREF pencolor;//畫筆顏色 COLORREF c; CPoint start,end;//直線、矩形和橢圓的起始點 int style;//圖形的類型 CPoint angle;//圓角矩形角度 };
為文檔Doc類添加Gpen的鏈表:
CList <GPen,GPen> Mylist;
文檔類的串行化Serialize函數:
void CWkfDrawingDoc::Serialize(CArchive& ar) { int i; if (ar.IsStoring())//保存 { // TODO: 在此添加存儲代碼 ar<<Mylist.GetCount(); GPen g; POSITION pos = Mylist.GetHeadPosition(); for(i = 0; i<Mylist.GetCount(); i++) { g = Mylist.GetNext(pos); ar<<g.type<<g.width<<g.pencolor<<g.c<<g.start<<g.end<<g.style<<g.angle; } } else//讀取 { // TODO: 在此添加加載代碼 int count; ar>>count; GPen g; POSITION pos = Mylist.GetHeadPosition(); for(i = 0; i<count; i++) { ar>>g.type>>g.width>>g.pencolor>>g.c>>g.start>>g.end>>g.style>>g.angle; Mylist.AddTail(g); } } }
打開之前保存文件需要有一個重繪函數,我們之前畫圖都只是在鼠標移動和鼠標左鍵抬起的時候畫圖,現在畫圖都要在視圖類中的OnDraw中重繪了,這也就是之前的鼠標左鍵抬起消息響應函數中,最后需要將所畫的圖形信息保存到鏈表中的原因了。(鼠標抬起了,這個圖形才真正被畫出來)
GetDocument()->Mylist.AddTail(g);//保存信息 Invalidate();
之后的哪一行代碼就是刷新,去執行OnDraw函數了。
void CWkfDrawingView::OnDraw(CDC* pDC)//加載文件重繪函數 { int i; CWkfDrawingDoc* pDoc = GetDocument(); pdc=new CClientDC(this); ASSERT_VALID(pDoc); if (!pDoc) return; GPen g; POSITION pos = pDoc->Mylist.GetHeadPosition(); for(i = 0; i<pDoc -> Mylist.GetCount(); i++) { g = pDoc -> Mylist.GetNext(pos); CPen p(g.type,g.width,g.pencolor); pdc->SelectObject(&p); pdc->MoveTo(g.start.x,g.start.y); if(g.style==1)//畫直線 { pdc->SelectStockObject(NULL_BRUSH); pdc->LineTo(g.end.x,g.end.y); } if(g.style==2)//畫矩形 { pdc->SelectStockObject(NULL_BRUSH); pdc->Rectangle(g.start.x,g.start.y,g.end.x,g.end.y); } if(g.style==3)//畫圓形 { pdc->SelectStockObject(NULL_BRUSH); pdc->Ellipse(g.start.x,g.start.y,g.end.x,g.end.y); } if(g.style==4)//畫填充矩形 { CBrush bsh; bsh.CreateSolidBrush(g.pencolor); pdc->SelectObject(&bsh); pdc->Rectangle(g.start.x,g.start.y,g.end.x,g.end.y); bsh.DeleteObject(); } if(g.style==5)//畫填充圓形 { CBrush bsh; bsh.CreateSolidBrush(g.pencolor); pdc->SelectObject(&bsh); pdc->Ellipse(g.start.x,g.start.y,g.end.x,g.end.y); bsh.DeleteObject(); } if(g.style==6)//畫圓角矩形 { pdc->SelectStockObject(NULL_BRUSH); pdc->RoundRect(g.start.x,g.start.y,g.end.x,g.end.y,g.angle.x,g.angle.y); } if(g.style==7)//畫填充圓角矩形 { CBrush bsh; bsh.CreateSolidBrush(g.pencolor); pdc->SelectObject(&bsh); pdc->RoundRect(g.start.x,g.start.y,g.end.x,g.end.y,g.angle.x,g.angle.y); bsh.DeleteObject(); } pdc->SelectObject(OldPen); } }
五、幾個對話框
顏色對話框和字體對話框是系統給的,我這里給出其按鍵的消息響應函數。其中的MyFont和是Pcolor是視圖類的兩個成員變量CFont MyFont COLORREF Pcolor
void CWkfDrawingView::OnFont()//字體設置 { CFontDialog dlg; if(IDOK==dlg.DoModal()) { if(MyFont.m_hObject) { MyFont.DeleteObject(); } MyFont.CreateFontIndirect(dlg.m_cf.lpLogFont);//字體信息 MyFontName=dlg.m_cf.lpLogFont->lfFaceName;//字體的名稱 } } void CWkfDrawingView::OnPancolor()//畫筆顏色設置 { CColorDialog dlg(0,CC_FULLOPEN); if(dlg.DoModal()) { Pcolor = dlg.GetColor();//從顏色對話框中獲取顏色信息 GP.pencolor=Pcolor; } else if(dlg.DoModal()==IDCANCEL) {} }
對於自定義的對話框,有畫筆的寬度設置,畫筆類型設置,文本輸入,代碼如下:
void CWkfDrawingView::OnTxt()//文本輸入 { // TODO: 在此添加命令處理程序代碼 CTxtlog dlg; if(dlg.DoModal()==IDOK) { int X=dlg.MyX; int Y=dlg.MyY; CString String=dlg.MyString; pdc=new CClientDC(this);//構造對象 pdc->SetTextColor(GP.pencolor);//設置文件顏色 pdc->SelectObject(&MyFont); pdc->TextOut(X,Y,String); } else if(dlg.DoModal()==IDCANCEL) {} } void CWkfDrawingView::OnLineW()//畫筆寬度 { // TODO: 在此添加命令處理程序代碼 CLWidth dlg; if(dlg.DoModal()==IDOK) { GP.width=dlg.width;//更新畫筆的寬度 MyWidth=dlg.width; } else if(dlg.DoModal()==IDCANCEL) {} } /* PS_SOLID 實線 PS_DASH 虛線 PS_DOT 點線 PS_DASHDOT 點化線 PS_DASHDOTDOT 雙點化線 */ void CWkfDrawingView::OnSolid()//線條類型 { // TODO: 在此添加命令處理程序代碼 type=PS_SOLID; GP.type=type; pdc=new CClientDC(this); } void CWkfDrawingView::OnDash() { // TODO: 在此添加命令處理程序代碼 type=PS_DASH; GP.type=type; } void CWkfDrawingView::OnDot() { // TODO: 在此添加命令處理程序代碼 type=PS_DOT; GP.type=type; } void CWkfDrawingView::OnDashdot() { // TODO: 在此添加命令處理程序代碼 type=PS_DASHDOT; GP.type=type; } void CWkfDrawingView::OnDashdotdot() { // TODO: 在此添加命令處理程序代碼 type=PS_DASHDOTDOT; GP.type=type; }
但對於文本輸入和字體寬度設置,需要從對話框中獲取信息,保存到變量中,這就需要交換函數,在這之前需要將自定義的對話框設置一個對話框類,與對話框資源相關聯,所有的代碼處理都在對話框類中進行。以文本輸入為例,添加文本輸入對話框類
#pragma once // CTxtlog 對話框 class CTxtlog : public CDialog { DECLARE_DYNAMIC(CTxtlog) public: CTxtlog(CWnd* pParent = NULL); // 標准構造函數 virtual ~CTxtlog(); // 對話框數據 enum { IDD = IDD_TEXT }; protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持 DECLARE_MESSAGE_MAP() public: int MyX; public: int MyY; public: CString MyString; };
修改其中的數據交換函數為:
void CTxtlog::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); DDX_Text(pDX, ID_TXTX, MyX); DDX_Text(pDX, ID_TXTY, MyY); DDX_Text(pDX, ID_TXTS, MyString); }