近期學習了MFC的相關知識,MFC(Microsoft Foundation Classes)是微軟公司提供的一個類庫,可以這樣簡單理解,就是對於Win32的封裝(MFC對windows API函數的封裝),但是MFC主要還是引入面向對象的開發思維,即一切用對象進行調用,我認為對鞏固C++面向對象的思維有很大的幫助,所以進行了學習。並且開發了一個具有簡單功能的截圖工具,因為之前學習過一段時間的WIn32知識,所以在接下來的敘述中,主要以 對比Win32和C++的相關思想 為介入點進行介紹。
MFC介紹(有MFC基礎的朋友,可以跳過這一段)
1.消息映射表
在WIn32中的消息響應是在消息循環中,通過Switch-case來實現的,其實在MFC中,消息循環這一部分用遍歷數組來代替了,並且在各個類中加入了 DECLARE_MESSAGE_MAP(),BEGIN_MESSAGE_MAP(),END_MESSAGE_MAP(),這個數組里存放了各種消息對應的函數指針,而且將消息和消息處理函數構成了一個映射表,又叫消息映射表。這方面和博客中另一篇文章 動態創建數組有異曲同工之妙。
2.MFC框架
MFC的框架由系統先搭好的窗口和CWinApp,CFrameWnd,CView,CDocument這些類構成,而這些類層次,我總結如下圖所示
3.MFC消息分類
這個和Win32中消息有所不同,不同於鍵盤,鼠標這些客觀發送的消息類型,在MFC中分為標准/系統消息,命令消息,控件/通知消息,自定義消息,同樣我也總結了分別在哪使用,和如何進行添加,如圖:
在這需要注意的一點就是在自定義消息中,用戶發送的兩個函數有實現上的區別,SendMessage()是直接發送到目標類中,而PostMessage()是發送到消息隊列中,由目標類自己去截獲,消息少時沒有影響,又稱為同步和異步消息投放函數。
接下來就介紹一下,這個截屏工具的開發思想,首先獲取桌面屏幕信息,再將桌面信息存入客戶區內,沒錯就是這么簡單,附加的功能就是可以畫圖和撤銷
截圖工具開發
1.修改MFC框架中窗口樣式
這個因人而異,我是刪除了狀態欄,工具欄,邊框(WS_POPUP)並且彈出就是全屏狀態(值得注意的是在MFC中一個變量就表示了多種樣式,微軟的開發人員用位運算或來加各種屬性,我相信日常開發人員在加屬性時,這也是日常操作,所以在修改時,刪去屬性用異或,或者取反與也可以)
2.添加一系列控件,並且加入消息處理函數
這些都屬於命令消息,需要注意的是在給關閉控件添加消息的時候,不能直接在關閉消息函數里就直接PostQuitMessage(),因為直接調用的話會出現內存泄漏,如下圖:
因為這就是關閉消息的過程,WM_CLOSE->destroy->...->PostQuitMessage,由此可以看出最后一步才是直接發送退出消息,所以在點擊退出的同時,給主窗口(CMainFrme類)發送一個關閉的消息
this->PostMessage(WM_CLOSE);
3.截屏
接下來就是截屏工具的主要功能,我分為三步,第一步,拿去桌面圖片,第二步,保存,第三步,貼圖至客戶區
在這就要介紹hdc在MFC中的封裝,hdc是一種包含有關某個設備(如顯示器或打印機)的繪制屬性信息的 Windows 數據結構。所有繪制調用都通過設備上下文對象進行,說白了就相當於畫板,而在MFC中將GetDC,ReleaseDC這個函數都封裝在CDC這些類的構造和析構中,除了不用手動調用創建和釋放以外,還不用獲取窗口句柄了,誰調用就是獲取誰的,我也總結了一些常用的DC類,
CClientDC dc(this) | 視圖客戶區 |
CClientDC dc(AfxGetMainWnd()) | 獲取主窗口 |
CWindowDC dc(AfxGetMainWnd()) | 窗口 |
CWindowDC dc(GetDesktopWindow()) | 桌面窗口 |
1 CMainFrame::CMainFrame() 2 { 3 //============獲取桌面圖片 保存=============== 4 5 //--------------獲取屏幕參數---------------- 6 m_nScreenCX = ::GetSystemMetrics(SM_CXSCREEN); 7 m_nSCreenCY = ::GetSystemMetrics(SM_CYSCREEN); 8 //--------------獲取屏幕參數---------------- 9 10 //------------- 獲取桌面圖片---------------- 11 CWindowDC DesktopDC(GetDesktopWindow()); 12 CDC cdc; 13 cdc.CreateCompatibleDC(&DesktopDC); 14 15 CBitmap *bitmap = new CBitmap; 16 bitmap->CreateCompatibleBitmap(&DesktopDC,m_nScreenCX,m_nSCreenCY); 17 cdc.SelectObject(bitmap); 18 cdc.BitBlt(0,0,m_nScreenCX,m_nSCreenCY,&DesktopDC,0,0,SRCCOPY); 19 //------------- 獲取桌面圖片---------------- 20 21 22 //-----------------保存-------------------- 23 sk.push(bitmap); 24 25 26 27 //-----------------保存-------------------- 28 29 //============獲取桌面圖片 保存=============== 30 }
值得注意的是,我選擇了棧作為保存方式,主要是因為我考慮到之后要寫撤銷的功能,而之后事實證明,利用棧這一數據結構來保存好處不止這一點,此時的效果如圖
4.畫圖控件處理
我添加了 曲線,直線,長方形,圓形,三角形這些畫圖功能,在MFC中 ON_COMMAND_RANGE(起始ID,終止ID,處理函數地址)這個函數可以此命令可以處理相同操作的ID,因為無論什么形狀,最終的功能就是畫圖,接下來就是如何判斷點擊的是何種控件,在這個地方用if去判斷也可以,我利用資源中這個控件圖片宏的連續性(不連續可以改連續),和枚舉來進行判斷,避免了大量的if判斷
1 enum {PEN,LINE,RETANGLE,CIRCULAR,TANGLE}; 2 3 void CCUTBITMAPView::OnChooseTool(UINT nID) 4 { 5 m_nDrawStyle = nID - ID_TOOL_PEN; 6 }
接下來就是畫圖功能,來分析一下,在選擇后鼠標按下開始畫,鼠標松開時結束畫,所以添加了WM_LBUTTONDOWN,WM_LBUTTONUP,WM_MOUSEMOVE三個命令消息,並且建立一個標記來記錄鼠標是否按下了,不然的話,打開后不用點擊就會自動開始畫,除了曲線以外都可以用MFC庫函數來進行查詢(簡單練習不建議使用太復雜的多邊形,因為到最后就是復雜的高中幾何題呀),曲線的畫法就像微積分中的微分思維一樣,當一條曲線切割的足夠短時,就是N多條直線組成,那么只要起始和結束坐標切換足夠快的話就是曲線了。
5.bug處理
到這一步就可以在截屏的圖案上進行繪畫了,但是會發現之前畫的痕跡並沒有消除,有點3D的感覺,哈哈哈,如何處理這個問題呢,重復貼圖,利用之前的棧頂元素來擦除痕跡,接下來痕跡是沒有了,但是不停的在閃爍,常見問題--閃爍就用雙緩沖來解決吧,不過MFC和WIn32這個思維轉化的有點困難,對象調用的思維還得多熟悉熟悉,把代碼貼上來,有MFC的初學者也可以看看
1 void CCUTBITMAPView::OnMouseMove(UINT nFlags, CPoint point) 2 { 3 CMainFrame *pFrame = (CMainFrame*)AfxGetMainWnd(); 4 if(m_bIsLButtonDown == true) 5 { 6 CClientDC dc(this); 7 if(m_nDrawStyle == PEN) 8 { 9 dc.MoveTo(FirstPoint); 10 dc.LineTo(point); 11 FirstPoint = point; 12 return; 13 } 14 //------------雙緩沖------------ 15 CDC hMenDC; 16 hMenDC.CreateCompatibleDC(&dc); 17 CBitmap bitmap; 18 bitmap.CreateCompatibleBitmap(&dc,pFrame->m_nScreenCX,pFrame->m_nSCreenCY); 19 hMenDC.SelectObject(bitmap); 20 21 22 //------------雙緩沖------------ 23 24 //-----------用棧頂元素 擦除軌跡------------ 25 CDC cdc; 26 cdc.CreateCompatibleDC(&dc); 27 cdc.SelectObject(pFrame->sk.top()); 28 hMenDC.BitBlt(0,0,pFrame->m_nScreenCX,pFrame->m_nSCreenCY,&cdc,0,0,SRCCOPY); 29 //-----------用棧頂元素 擦除軌跡------------ 30 switch (m_nDrawStyle) 31 { 32 case LINE: 33 { 34 hMenDC.MoveTo(FirstPoint); 35 hMenDC.LineTo(point); 36 } 37 break; 38 case RETANGLE: 39 { 40 hMenDC.Rectangle(FirstPoint.x,FirstPoint.y,point.x,point.y); 41 } 42 break; 43 case CIRCULAR: 44 { 45 hMenDC.Ellipse(FirstPoint.x,FirstPoint.y,point.x,point.y); 46 } 47 break; 48 case TANGLE: 49 { 50 POINT tangle[3] = { 51 {(FirstPoint.x+point.x)/2,FirstPoint.y}, 52 {point.x,point.y}, 53 {FirstPoint.x,point.y} 54 }; 55 hMenDC.Polygon(tangle,3); 56 } 57 break; 58 } 59 dc.BitBlt(0,0,pFrame->m_nScreenCX,pFrame->m_nSCreenCY,&hMenDC,0,0,SRCCOPY); 60 } 61 62 CView::OnMouseMove(nFlags, point); 63 }
但是每一次畫的,第二次點擊,都會覆蓋掉,解決這個,就是和當時截取桌面畫面一樣,只不過這次在鼠標抬起時,截取客戶區,存入棧中,同樣每次拿棧頂消除痕跡就會留下痕跡了,代碼如下
1 void CCUTBITMAPView::OnLButtonUp(UINT nFlags, CPoint point) 2 { 3 m_bIsLButtonDown = false; 4 //----------------------留下之前的痕跡----------------------- 5 CMainFrame *pFrame = (CMainFrame*)AfxGetMainWnd(); 6 7 8 CClientDC dc(this); 9 10 CDC cdc; 11 cdc.CreateCompatibleDC(&dc); 12 CBitmap *pBitMap = new CBitmap; 13 pBitMap->CreateCompatibleBitmap(&dc,pFrame->m_nScreenCX,pFrame->m_nSCreenCY); 14 cdc.SelectObject(pBitMap); 15 cdc.BitBlt(0,0,pFrame->m_nScreenCX,pFrame->m_nSCreenCY,&dc,0,0,SRCCOPY); 16 17 pFrame->sk.push(pBitMap); 18 //----------------------留下之前的痕跡----------------------- 19 CView::OnLButtonUp(nFlags, point); 20 }
6.撤銷功能
Ctrl+Z首先先添加到鍵盤映射表中
主要思路就是不停的將棧頂元素彈出棧中,就可以了,代碼如下
1 void CMainFrame::OnAccelerator32778() 2 { 3 //撤銷的函數 4 if(sk.size() > 1) 5 { 6 delete sk.top(); 7 sk.pop(); 8 GetActiveView()->SendMessage(WM_PAINT); 9 } 10 }
接下來,還有保存等其他的功能,未完待續。。。,先放一張實現的截圖
最后,記錄一個我覺得挺重要MFC的消息流向(因為知道,什么消息寫在哪個類中)
標准消息 | WM_XXX | 子類到父類,查找消息映射表 |
命令消息 | WM_COMMAND | 消息路由 CView-->CDoc-->CDocTemplate-->CFrameWnd-->CWinApp |
控件/通知消息 | WM_NOTIFY | 子窗口到父窗口,查找消息映射表 |
自定義消息 | UM_XXX | 確定了,查找調用者那個類 |
2019-07-21 19:17:07 編程小菜鳥自我總結,來往的朋友可以留下自己的建議和意見,謝謝!!!