您好,登錄后才能下訂單哦!
如何分析C++實(shí)現(xiàn)功能齊全的屏幕截圖示例,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來學(xué)習(xí)下,希望你能有所收獲。
屏幕截圖已經(jīng)成為了所有IM即時通訊軟件的必備模塊,也是日常辦公中使用最頻繁的功能之一。今天我們從C++開發(fā)的角度,來看看屏幕截圖的主要功能點(diǎn)是如何實(shí)現(xiàn)的,在此給大家分享一下屏幕截圖的諸多實(shí)現(xiàn)細(xì)節(jié)。
開發(fā)工具:Visual Studio 2010
開發(fā)語言:C++
UI框架:MFC(也可以基于開源的duilib框架,其實(shí)在duilib中是調(diào)用Windows API)
要使用屏幕截圖,其實(shí)很容易,裝一款聊天軟件或者辦公軟件就可以了,比如QQ、企業(yè)微信、釘釘、飛書等。但要開發(fā)出類似這些軟件的屏幕截圖模塊,則沒那么容易。其實(shí)實(shí)現(xiàn)屏幕截圖的技術(shù)并不復(fù)雜,主要是在各個細(xì)節(jié)問題的處理上。
有人可能會說,我并不需要自己開發(fā)這些功能,我可以去搜一些開源的代碼,也可以到網(wǎng)上搜一堆關(guān)于屏幕截圖的文章或下載資源,應(yīng)該可以找到能用的代碼或資源了。我想說的是,你大可以去試一試,很多都只是講到了一點(diǎn)皮毛,基本沒有一個實(shí)現(xiàn)了完備的截圖功能,沒有一個能拿到實(shí)際的項(xiàng)目中去使用的。簡單的寫幾句代碼,玩玩還可以,離真正商用到項(xiàng)目中,差的太遠(yuǎn)了!真正項(xiàng)目級的代碼,要考慮各種場景和細(xì)節(jié),要考慮性能和穩(wěn)定性,是經(jīng)過多輪測試錘煉出來的,不是隨便寫寫就能搞出來的!
本文將結(jié)合開發(fā)屏幕截圖的實(shí)際項(xiàng)目經(jīng)歷,詳細(xì)介紹一下屏幕截圖各個主要功能點(diǎn)的實(shí)現(xiàn)細(xì)節(jié)與方法,給大家提供一個借鑒和參考。
一個具有完備功能的屏幕截圖應(yīng)該包含以上多個功能點(diǎn),比如桌面灰化、窗口自動套索、區(qū)域放大、矩形等多個圖元繪制、輸入文字等。
網(wǎng)上很難找到一篇詳細(xì)介紹屏幕截圖完整功能的實(shí)現(xiàn)思路的,那屏幕截圖的主體實(shí)現(xiàn)思路到底是什么樣子的呢?下面我們就來簡單地描述一下。我們實(shí)現(xiàn)的一套屏幕截圖的效果如下(文章末尾處提供C++源碼下載):
下面基于我們實(shí)現(xiàn)的屏幕截圖,詳細(xì)介紹一下屏幕截圖主要的一些功能點(diǎn)和實(shí)現(xiàn)思路。
我們需要創(chuàng)建一個截圖的主窗口,開啟截圖后將該截圖主窗口全屏,覆蓋整個屏幕,并且給窗口設(shè)置TopMost置頂屬性。然后我們后續(xù)操作都是在這個全屏置頂?shù)拇翱谏线M(jìn)行繪圖出來的,即截圖時截圖窗口中看到的所有內(nèi)容(比如桌面灰化、窗口套索、區(qū)域放大、各個圖元等)都是繪制上去的!
在開啟截圖時,先將當(dāng)前桌面上的圖像保存到位圖對象中,保存兩份位圖,一份是亮色的桌面圖像,一份是經(jīng)過灰化后的桌面圖像。先將灰化的位圖繪制到截圖對話框上,實(shí)現(xiàn)灰化的遮罩。然后根據(jù)用戶拉動鼠標(biāo)選擇的區(qū)域,從亮色位圖中摳出對應(yīng)區(qū)域的亮色圖像繪制到對話框上,就能達(dá)到區(qū)域選擇的效果了。
在啟動截圖時,需要遍歷當(dāng)前系統(tǒng)中所有打開的窗口,以及這些窗口中的子窗口,把這些窗口的坐標(biāo)位置記錄下來保存到內(nèi)存中。當(dāng)鼠標(biāo)移動時,看鼠標(biāo)移動到哪個最上層的窗口,然后在該窗口的區(qū)域繪制上套索的邊界,并將該窗口區(qū)域“亮”起來。亮起來其實(shí)很簡單,根據(jù)該窗口的坐標(biāo)到內(nèi)存中保存的亮色位圖中將對應(yīng)的區(qū)域摳出來,繪制到窗口上,然后再在窗口邊界上繪制出套索邊界線即可。
其實(shí)實(shí)現(xiàn)這個功能并不難,可以仔細(xì)觀察以下主流IM軟件的顯示細(xì)節(jié),就能找到思路和答案了!區(qū)域放大是實(shí)時地將鼠標(biāo)移動到的位置的周圍區(qū)域放大,放大的區(qū)域是以鼠標(biāo)點(diǎn)為中心的一小片矩形區(qū)域,然后將該區(qū)域放大4倍,將放大的效果繪制到截圖對話框上。
可以使用微軟MFC庫中提供的橡皮筋類CRectTracker來實(shí)現(xiàn)區(qū)域的選擇。該橡皮筋類對應(yīng)一個選擇邊框,通過拉動鼠標(biāo),繪制出選擇區(qū)域的橡皮筋邊框,橡皮筋邊框支持拖動,改變橡皮筋邊框的大小。根據(jù)橡皮經(jīng)選擇的區(qū)域,到內(nèi)存中保存的亮色位圖中摳出亮色選擇區(qū)域,繪制到截圖窗口上就好了。
截圖工具條一般做成一個緊貼截圖選擇區(qū)域的窗口,窗口中包含一排功能按鈕,一般包括矩形工具、橢圓工具、帶箭頭直線工具、曲線工具、Undo工具、關(guān)閉截圖、完成截圖這幾個功能按鈕。選擇矩形工具、橢圓工具、帶箭頭直線工具和曲線工具這四個按鈕后,鼠標(biāo)在截圖窗口上繪制的就是對應(yīng)類型的圖元。Undo按鈕是回撤上一次繪制的圖元。
我們需要設(shè)計(jì)圖元類型對應(yīng)的C++類,這些類統(tǒng)一繼承于一個CSharp的基類,基類中保存當(dāng)前繪制圖元的線條顏色、起點(diǎn)和終點(diǎn)坐標(biāo),還有一個用于繪制圖元內(nèi)容的純虛接口Draw,具體的Draw操作都在具體的圖元中實(shí)現(xiàn)。這其實(shí)使用C++中多態(tài)的概念。
對于矩形、橢圓和帶箭頭的直線,我們只需要記錄圖元的起點(diǎn)和終點(diǎn)坐標(biāo)就可以了,對于曲線,則由多個直線線段構(gòu)成的,我們要記錄繪制過程中的多個點(diǎn)。當(dāng)用戶左鍵按下時開始繪制圖元,記錄此時圖元起點(diǎn)坐標(biāo),在左鍵彈起時截圖當(dāng)前圖元的繪制,記錄圖元的終點(diǎn)坐標(biāo),然后創(chuàng)建對應(yīng)類型的圖元對象,將起點(diǎn)及終點(diǎn)坐標(biāo)保存到對象中,然后把這些圖元對象保存到圖元列表中。窗口需要刷新時,調(diào)用列表中這些圖像的Draw接口將所有圖元繪制到截圖窗口上。
開啟截圖時,將桌面的圖像保存到亮色位圖對象中,同時對圖像進(jìn)行灰化處理,將處理后的圖像保存到暗色位圖對象中。保存桌面圖像的代碼如下所示:
// 拷貝桌面,lpRect 代表選定區(qū)域,bSave 標(biāo)記是否將圖片內(nèi)容保存到剪切板中 HBITMAP CScreenCatchDlg::CopyScreenToBitmap( LPRECT lpRect ) { // 確保選定區(qū)域不為空矩形 if ( IsRectEmpty( lpRect ) ) { return NULL; } CString strLog; // 為屏幕創(chuàng)建設(shè)備描述表 HDC hScrDC = ::CreateDC( _T("DISPLAY"), NULL, NULL, NULL ); if ( hScrDC == NULL ) { strLog.Format( _T("[CCatchScreenDlg::CopyScreenToBitmap] 創(chuàng)建DISPLAY失敗, GetLastError: %d"), GetLastError() ); WriteScreenCatchLog( strLog ); return NULL; } // 為屏幕設(shè)備描述表創(chuàng)建兼容的內(nèi)存設(shè)備描述表 HDC hMemDC = ::CreateCompatibleDC( hScrDC ); if ( hMemDC == NULL ) { strLog.Format( _T("[CCatchScreenDlg::CopyScreenToBitmap]創(chuàng)建與hScrDC兼容的hMemDC失敗, GetLastError: %d"), GetLastError() ); WriteScreenCatchLog( strLog ); ::DeleteDC( hScrDC ); return NULL; } int nX = 0; int nY = 0; int nX2 = 0; int nY2 = 0; int nWidth = 0; int nHeight = 0; // 保證left小于right,top小于bottom LONG lTemp = 0; if ( lpRect->left > lpRect->right ) { lTemp = lpRect->left; lpRect->left = lpRect->right; lpRect->right = lTemp; } if ( lpRect->top > lpRect->bottom ) { lTemp = lpRect->top; lpRect->top = lpRect->bottom; lpRect->bottom = lTemp; } // 獲得選定區(qū)域坐標(biāo) nX = lpRect->left; nY = lpRect->top; nX2 = lpRect->right; nY2 = lpRect->bottom; // 確保選定區(qū)域是可見的 if ( nX < 0 ) { nX = 0; } if ( nY < 0 ) { nY = 0; } if ( nX2 > m_xScreen ) { nX2 = m_xScreen; } if ( nY2 > m_yScreen ) { nY2 = m_yScreen; } nWidth = nX2 - nX; nHeight = nY2 - nY; // 創(chuàng)建一個與屏幕設(shè)備描述表兼容的位圖 HBITMAP hBitmap = ::CreateCompatibleBitmap( hScrDC, nWidth, nHeight ); if ( hBitmap == NULL ) { strLog.Format( _T("[CCatchScreenDlg::CopyScreenToBitmap]創(chuàng)建與hScrDC兼容的Bitmap失敗, GetLastError: %d"), GetLastError() ); WriteScreenCatchLog( strLog ); ::DeleteDC( hScrDC ); ::DeleteDC( hMemDC ); return NULL; } // 把新位圖選到內(nèi)存設(shè)備描述表中 ::SelectObject( hMemDC, hBitmap ); BOOL bRet = ::BitBlt( hMemDC, 0, 0, nWidth, nHeight, hScrDC, nX, nY, SRCCOPY | CAPTUREBLT ); // CAPTUREBLT - 該參數(shù)保證能夠截到透明窗口 if ( !bRet ) { strLog.Format( _T("[CCatchScreenDlg::CopyScreenToBitmap]將hScrDC拷貝到hMemDC失敗, GetLastError: %d"), GetLastError() ); WriteScreenCatchLog( strLog ); ::DeleteDC( hScrDC ); ::DeleteDC( hMemDC ); ::DeleteObject( hBitmap ); return NULL; } if ( hScrDC != NULL ) { ::DeleteDC( hScrDC ); } if ( hMemDC != NULL ) { ::DeleteDC( hMemDC ); } return hBitmap; // hBitmap資源不能釋放,因?yàn)楹瘮?shù)外部要使用 }
如何將桌面圖像進(jìn)行灰化處理呢?其實(shí)很簡單,只要將保存的桌面位圖中的每個像素值的RGB讀出來,將每個像素中的R、G、B值都乘以一個系數(shù),然后再將這些值設(shè)置回位圖中即可,相關(guān)代碼如下:
void CScreenCatchDlg::GrayLightBmp() { CString strLog; CDC *pDC = GetDC(); ASSERT( pDC ); if ( pDC == NULL ) { strLog.Format( _T("[CCatchScreenDlg::DoGrayLightBmp] GetDC失敗, GetLastError: %d"), GetLastError() ); WriteScreenCatchLog( strLog ); return; } CBitmap cbmp; cbmp.Attach( m_hGreyBitmap ); // 此處使用臨時保存亮色位圖的m_hDarkBitmap BITMAP bmp; cbmp.GetBitmap( &bmp ); cbmp.Detach(); // 需要將對象和句柄分離,m_hDarkBitmap位圖資源需要保存在內(nèi)存中,如不分離,則當(dāng)對象消亡時,m_hDarkBitmap位圖資源會自動被釋放掉 UINT *pData = new UINT[bmp.bmWidth * bmp.bmHeight]; if ( pData == NULL ) { int nSize = bmp.bmWidth * bmp.bmHeight; strLog.Format( _T("[CCatchScreenDlg::DoGrayLightBmp]pData通過new申請%s字節(jié)的內(nèi)存失敗,直接return"), nSize ); WriteScreenCatchLog( strLog ); ReleaseDC( pDC ); return; } BITMAPINFO bmpInfo; bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmpInfo.bmiHeader.biWidth = bmp.bmWidth; bmpInfo.bmiHeader.biHeight = -bmp.bmHeight; bmpInfo.bmiHeader.biPlanes = 1; bmpInfo.bmiHeader.biCompression = BI_RGB; bmpInfo.bmiHeader.biBitCount = 32; int nRet = GetDIBits( pDC->m_hDC, m_hGreyBitmap, 0, bmp.bmHeight, pData, &bmpInfo, DIB_RGB_COLORS ); if ( 0 == nRet ) { strLog.Format( _T("[CCatchScreenDlg::DoGrayLightBmp]GetDIBits失敗 nRet == 0, GetLastError: %d"), GetLastError() ); WriteScreenCatchLog( strLog ); } // 將圖像中的所有像素點(diǎn)的RGB值都乘以0.4,即實(shí)現(xiàn)了圖像的灰化 UINT color, r, g, b; for ( int i = 0; i < bmp.bmWidth * bmp.bmHeight; i++ ) { color = pData[i]; b = ( color << 8 >> 24 ) * 0.4; g = ( color << 16 >> 24 ) * 0.4; r = ( color << 24 >> 24 ) * 0.4; pData[i] = RGB(r, g, b); } // 如果函數(shù)成功,那么返回值就是復(fù)制的掃描線數(shù);如果函數(shù)失敗,那么返回值是0。 nRet = SetDIBits( pDC->m_hDC, m_hGreyBitmap, 0, bmp.bmHeight, pData, &bmpInfo, DIB_RGB_COLORS ); if ( 0 == nRet ) { strLog.Format( _T("[CCatchScreenDlg::DoGrayLightBmp]SetDIBits失敗 nRet == 0, GetLastError: %d"), GetLastError() ); WriteScreenCatchLog( strLog ); } delete []pData; pData = NULL; ReleaseDC( pDC ); }
內(nèi)存中要保留兩份位圖,一份是亮色的桌面圖像,一份是經(jīng)過灰化后的桌面圖像。先將灰化的位圖繪制到截圖對話框上,實(shí)現(xiàn)灰化的遮罩。然后根據(jù)用戶拉動鼠標(biāo)選擇的區(qū)域,從亮色位圖中摳出對應(yīng)區(qū)域的亮色圖像繪制到對話框上,就能達(dá)到區(qū)域選擇的效果了。
在啟動截圖時,需要遍歷當(dāng)前系統(tǒng)中所有打開的窗口,以及這些窗口中的子窗口,把這些窗口的坐標(biāo)位置記錄下來保存到內(nèi)存中。先調(diào)用系統(tǒng)API函數(shù)EnumWindows,將系統(tǒng)中打開的窗口都枚舉出來:
// 使用EnumWindows來枚舉當(dāng)前系統(tǒng)打開的所有大窗口 ::EnumWindows( EnumWindowsProc, NULL ); BOOL CEnumWindows::EnumWindowsProc( HWND hWnd, LPARAM lParam ) { TCHAR achWndName[MAX_PATH+1] = {0}; if ( ::IsWindow(hWnd) && ::IsWindowVisible(hWnd) && !::IsIconic(hWnd) ) { // 保存所有有效窗口 EnumedWindowInfo tWndInfo; tWndInfo.m_hWnd = hWnd; ::GetWindowText( hWnd, achWndName, sizeof(achWndName)/sizeof(TCHAR) ); tWndInfo.m_strWndName = achWndName; 將桌面區(qū)域過濾掉 //if ( !_tcscmp( tWndInfo.m_strWndName, _T("Program Manager") ) ) //{ // return TRUE; //} ::GetWindowRect( hWnd, &(tWndInfo.m_rcWnd) ); m_listWindows.push_back( tWndInfo ); } return TRUE; }
然后再遍歷這些窗口,使用遞歸調(diào)用的方式找出這些主窗口的各個子窗口,記錄下這些子窗口的信息。
當(dāng)鼠標(biāo)移動時,根據(jù)鼠標(biāo)的位置坐標(biāo),到窗口信息列表中去遍歷,看鼠標(biāo)移動到哪個最上層的窗口,然后在該窗口的區(qū)域繪制上套索的邊界,并將該窗口區(qū)域“亮”起來。亮起來其實(shí)很簡單,根據(jù)該窗口的坐標(biāo)到內(nèi)存中保存的亮色位圖中將對應(yīng)的區(qū)域摳出來,繪制到窗口上,然后再在窗口邊界上繪制出套索邊界線即可。
實(shí)現(xiàn)這點(diǎn)也不難,可以仔細(xì)觀察以下主流IM軟件的顯示細(xì)節(jié),就能找到思路與方法了!區(qū)域放大是實(shí)時地將鼠標(biāo)移動到的位置的周圍區(qū)域放大,放大的區(qū)域是以鼠標(biāo)點(diǎn)為中心的一小片矩形區(qū)域。
確定待放大區(qū)域的坐標(biāo)后,從內(nèi)存中保存的桌面亮色位圖中摳出亮色的待放大區(qū)域,然后調(diào)用StretchBlt將放大后的圖像繪制到截圖窗口上,相關(guān)代碼如下:
// 在內(nèi)存pMemDC中繪制自動套索窗口 void CScreenCatchDlg::DrawAutoLassoWndArea( CDC* pMemDC, CDC* pLightDC ) { if ( pMemDC == NULL || pLightDC == NULL ) { return; } if ( m_rcTargetWnd.IsRectEmpty() ) { return; } // 先從亮色圖片將目標(biāo)窗口摳出 CRect rcArea = m_rcTargetWnd; BOOL bRet = pMemDC->BitBlt( rcArea.left, rcArea.top, rcArea.Width(), rcArea.Height(), pLightDC, rcArea.left, rcArea.top, SRCCOPY ); if ( !bRet ) { WriteScreenCatchLog( _T("[CCatchScreenDlg::DrawAutoLassoWndPic]pMemDC->BitBlt(rcArea.left,rcArea.top…失敗") ); } rcArea.left = (rcArea.left-4<0) ? 4 : rcArea.left; rcArea.top = (rcArea.top-4<0) ? 4 : rcArea.top; rcArea.right = (rcArea.right+4>m_xScreen) ? (m_xScreen-4) : rcArea.right; rcArea.bottom = (rcArea.bottom+4>m_yScreen) ? (m_yScreen-4) : rcArea.bottom; // 再在目標(biāo)窗口周邊畫上自動套索邊界線 CPen pen( PS_SOLID, 1, RGB( 0, 174, 255 ) ); CPen* pOldPen = pMemDC->SelectObject( &pen ); CBrush* pOldBrush = ( CBrush* )pMemDC->SelectStockObject( NULL_BRUSH ); // 使用NULL_BRUSH調(diào)用SelectStockObject可以實(shí)現(xiàn)透明畫刷的效果 rcArea.InflateRect( 1, 1 ); pMemDC->Rectangle( &rcArea ); rcArea.InflateRect( 1, 1 ); pMemDC->Rectangle( &rcArea ); rcArea.InflateRect( 1, 1 ); pMemDC->Rectangle( &rcArea ); rcArea.InflateRect( 1, 1 ); pMemDC->Rectangle( &rcArea ); //rcArea.DeflateRect( 1, 1 ); //rcArea.DeflateRect( 1, 1 ); //pMemDC->Rectangle( &rcArea ); //rcArea.DeflateRect( 1, 1 ); //pMemDC->Rectangle( &rcArea ); //rcArea.DeflateRect( 1, 1 ); //pMemDC->Rectangle( &rcArea ); pMemDC->SelectObject( pOldBrush ); pMemDC->SelectObject( pOldPen ); }
微軟MFC庫中的橡皮筋類CRectTracker是個好東西,繪制出來的是個有邊框線的矩形邊界線,邊框線上有八個點(diǎn)可以用鼠標(biāo)點(diǎn)擊拖動來改變矩形邊界線的大小。我們正可以使用這個橡皮筋類來實(shí)現(xiàn)截圖區(qū)域的選擇。
橡皮筋類CRectTracker實(shí)現(xiàn)的有點(diǎn)復(fù)雜,也很巧妙,我們將該類的代碼從MFC庫中拿出來,對其進(jìn)行一些簡單靈活的改造,就可以用到截圖模塊中。添加一些消息通知和額外的處理機(jī)制。摳出來的類,我們命名為CCatchTracker,其頭文件如下所示:
/ // CCatchTracker - simple rectangular tracking rectangle w/resize handles // CCatchTracker類從MFC源文件COPY過來,根據(jù)自身的需要做了修改,對消息機(jī)制 // 做了點(diǎn)改動,增加了部分接口 #ifndef CATCH_SCREEN_TRACKER_H #define CATCH_SCREEN_TRACKER_H #define CX_BORDER 1 #define CY_BORDER 1 #define WM_UPDATE_TOOLBAR_POS ( WM_USER+700 ) // 更新截圖工具條位置消息,當(dāng)截取區(qū)域發(fā)生變化時要向界面發(fā)送該消息 #define CRIT_RECTTRACKER 5 void AFXAPI AfxLockGlobals(int nLockType); void AFXAPI AfxUnlockGlobals(int nLockType); void AFXAPI AfxDeleteObject(HGDIOBJ* pObject); enum TrackerHit { hitNothing = -1, hitTopLeft = 0, hitTopRight = 1, hitBottomRight = 2, hitBottomLeft = 3, hitTop = 4, hitRight = 5, hitBottom = 6, hitLeft = 7, hitMiddle = 8 }; class CCatchTracker { public: // Constructors CCatchTracker(); CCatchTracker(LPCRECT lpSrcRect, UINT nStyle); // Style Flags enum StyleFlags { solidLine = 1, dottedLine = 2, hatchedBorder = 4, resizeInside = 8, resizeOutside = 16, hatchInside = 32, resizeMiddle =80 //設(shè)置中間 }; // Hit-Test codes //enum TrackerHit //{ // hitNothing = -1, // hitTopLeft = 0, hitTopRight = 1, hitBottomRight = 2, hitBottomLeft = 3, // hitTop = 4, hitRight = 5, hitBottom = 6, hitLeft = 7, hitMiddle = 8 //}; // Operations void Draw(CDC* pDC) const; void GetTrueRect(LPRECT lpTrueRect) const; BOOL SetCursor(CWnd* pWnd, UINT nHitTest) const; BOOL Track(CWnd* pWnd, CPoint point, BOOL bAllowInvert =TRUE, CWnd* pWndClipTo = NULL); BOOL TrackRubberBand(CWnd* pWnd, CPoint point, BOOL bAllowInvert = TRUE); int HitTest(CPoint point) const; int NormalizeHit(int nHandle) const; // Overridables virtual void DrawTrackerRect(LPCRECT lpRect, CWnd* pWndClipTo, CDC* pDC, CWnd* pWnd); virtual void AdjustRect(int nHandle, LPRECT lpRect); virtual void OnChangedRect(const CRect& rectOld); virtual UINT GetHandleMask() const; // Implementation public: virtual ~CCatchTracker(); public: // 設(shè)置調(diào)整光標(biāo) void SetResizeCursor(UINT nID_N_S,UINT nID_W_E,UINT nID_NW_SE, UINT nID_NE_SW,UINT nIDMiddle); // 創(chuàng)建畫刷,內(nèi)部調(diào)用 void CreatePen(); // 設(shè)置矩形顏色 void SetRectColor(COLORREF rectColor); // 設(shè)置該矩形tracker是否可以移動,當(dāng)點(diǎn)擊截圖工具條中的按鈕后即不可移動 void SetMovable( BOOL bMoveable ); BOOL GetMovable(){ return m_bMovable; }; // implementation helpers int HitTestHandles(CPoint point) const; void GetHandleRect(int nHandle, CRect* pHandleRect) const; void GetModifyPointers(int nHandle, int**ppx, int**ppy, int* px, int*py); virtual int GetHandleSize(LPCRECT lpRect = NULL) const; BOOL TrackHandle(int nHandle, CWnd* pWnd, CPoint point, CWnd* pWndClipTo); void Construct(); void SetMsgHwnd(HWND hwnd); public: // Attributes UINT m_nStyle; // current state CRect m_rect; // current position (always in pixels) CSize m_sizeMin; // minimum X and Y size during track operation int m_nHandleSize; // size of resize handles (default from WIN.INI) BOOL m_bAllowInvert; // flag passed to Track or TrackRubberBand CRect m_rectLast; CSize m_sizeLast; BOOL m_bErase; // TRUE if DrawTrackerRect is called for erasing BOOL m_bFinalErase; // TRUE if DragTrackerRect called for final erase COLORREF m_rectColor; // 當(dāng)前矩形顏色 HWND m_hMsgWnd; // 向界面發(fā)送消息的窗口句柄 BOOL m_bMovable; // 標(biāo)記該矩形tracker是否可以移動,當(dāng)點(diǎn)擊截圖工具條中的按鈕后即不可移動 }; #endif
根據(jù)橡皮經(jīng)選擇的區(qū)域,到內(nèi)存中保存的亮色位圖中摳出亮色選擇區(qū)域,繪制到截圖窗口上就好了。截圖工具條是緊貼著橡皮筋選擇區(qū)域的,位于該區(qū)域的下方,當(dāng)橡皮筋區(qū)域大小發(fā)生變化時,要通知截圖工具條窗口跟著截圖區(qū)域一起動,使截圖工具條緊跟著橡皮筋選擇區(qū)域。所以我們在橡皮筋類中拋出如下的通知消息:
switch (msg.message) { // handle movement/accept messages case WM_LBUTTONUP: case WM_MOUSEMOVE: rectOld = m_rect; // handle resize cases (and part of move) if (px != NULL) *px = (int)(short)LOWORD(msg.lParam) - xDiff; if (py != NULL) *py = (int)(short)HIWORD(msg.lParam) - yDiff; // handle move case if (nHandle == hitMiddle) { m_rect.right = m_rect.left + nWidth; m_rect.bottom = m_rect.top + nHeight; } // 發(fā)送矩形區(qū)域的左上角和右下角的坐標(biāo)給界面,一方面在移動矩形時要用到, // 一方面在更新界面中的截圖工具條的位置時要用到 if ( IsWindow( m_hMsgWnd ) ) // 檢驗(yàn)是否是有效的窗口句柄 { BOOL bLBtnUp = FALSE; if ( msg.message == WM_LBUTTONUP ) { bLBtnUp = TRUE; } ::SendMessage(m_hMsgWnd, WM_UPDATE_TOOLBAR_POS, (WPARAM)&m_rect, (LPARAM)bLBtnUp ); }
截圖中要支持矩形、橢圓、帶箭頭直線和曲線四種圖元的繪制,我們分別設(shè)計(jì)了與圖元類型對應(yīng)的C++類,這些類統(tǒng)一繼承于一個CSharp的基類,基類中保存當(dāng)前繪制圖元的線條顏色、起點(diǎn)和終點(diǎn)坐標(biāo),還有一個用于繪制圖元內(nèi)容的純虛接口Draw:
// 形狀基類 class CShape { public: CShape(); virtual ~CShape(); virtual void Draw( CDC* pDC ) = 0; protected: CPoint m_startPt; // 起點(diǎn) CPoint m_endPt; // 終點(diǎn) COLORREF m_color; // 當(dāng)前使用顏色 };
具體的Draw操作都在具體的圖元中實(shí)現(xiàn)。這其實(shí)使用C++中多態(tài)的概念。
以矩形圖元為例,矩形類CRectangle的頭文件如下:
// 矩形 class CRectangle : public CShape { public: CRectangle( CPoint startPt, CPoint endPt ); ~CRectangle(); void Draw( CDC* pDC ); };
cpp源文件的代碼如下:
CRectangle::CRectangle( CPoint startPt, CPoint endPt ) { m_startPt = startPt; m_endPt = endPt; } CRectangle::~CRectangle() { } void CRectangle::Draw( CDC* pDC ) { if ( pDC == NULL ) { return; } Pen pen( Color(255, 0, 0), 2.0 ); pen.SetLineCap(LineCapRound, LineCapRound, DashCapRound); Graphics graphics( pDC->GetSafeHdc() ); //graphics.SetSmoothingMode( SmoothingModeAntiAlias ); //graphics.DrawRectangle( &pen, m_startPt.x, m_startPt.y, m_endPt.x-m_startPt.x, m_endPt.y-m_startPt.y ); CRect rcTemp( m_startPt.x, m_startPt.y, m_endPt.x, m_endPt.y ); rcTemp.NormalizeRect(); Status stRet = graphics.DrawRectangle( &pen, rcTemp.left, rcTemp.top, rcTemp.Width(), rcTemp.Height() ); }
對于矩形、橢圓和帶箭頭的直線,我們只需要記錄圖元的起點(diǎn)和終點(diǎn)坐標(biāo)就可以了,對于曲線,則由多個直線線段構(gòu)成的,我們要記錄繪制過程中的多個點(diǎn)。當(dāng)用戶左鍵按下時開始繪制圖元,記錄此時圖元起點(diǎn)坐標(biāo),在左鍵彈起時截圖當(dāng)前圖元的繪制,記錄圖元的終點(diǎn)坐標(biāo),然后創(chuàng)建對應(yīng)類型的圖元對象,將起點(diǎn)及終點(diǎn)坐標(biāo)保存到對象中,然后把這些圖元對象保存到圖元列表中。窗口需要刷新時,調(diào)用列表中這些圖像的Draw接口將所有圖元繪制到截圖窗口上。
最開始我們是使用GDI函數(shù)繪制圖元的,比如GDI中的API函數(shù)Reactangle(繪制矩形)、Ellipse(繪制橢圓)等,但在繪制帶箭頭的直線和曲線時,GDI函數(shù)繪制出來的結(jié)果中有明顯的鋸齒,效果很不好。所以后來我們將圖元的繪制全部改成使用GDI+庫來處理,GDI+中的Graphics類在繪制圖元時,可以設(shè)置反鋸齒的模式:
case emBtnEllipse: // 畫橢圓 { // 為了抗鋸齒,均使用GDI+來繪制圖元(GDI繪制直線和曲線時有明顯的鋸齒) Pen pen( Color(255, 0, 0), WIDTH_DRAW_PEN ); Graphics graphics( m_tmpDrawDC.GetSafeHdc() ); graphics.SetSmoothingMode( SmoothingModeAntiAlias ); graphics.DrawEllipse( &pen, m_drawStartPt.x/*-m_rectTracker.m_rect.left*/, m_drawStartPt.y/*-m_rectTracker.m_rect.top*/, point.x-m_drawStartPt.x/*+m_rectTracker.m_rect.left*/, point.y-m_drawStartPt.y/*+m_rectTracker.m_rect.top*/ ); } break;
整個全屏置頂?shù)慕貓D主窗口上面顯示的所有內(nèi)容都是都是我們在截圖窗口中繪制出來的,比如窗口的自動套索效果、區(qū)域放大效果、截圖區(qū)域的橡皮筋選擇框、各種圖元的繪制等。
我們要在截圖窗口上接管所有內(nèi)容的繪制,需要攔截截圖窗口的WM_ERASEBKGND和WM_PAINT消息。首先在收到WM_ERASEBKGND消息后,直接return TRUE,不需要系統(tǒng)幫我們繪制背景:
BOOL CScreenCatchDlg::OnEraseBkgnd( CDC* pDC ) { return TRUE; }
在收到WM_PAINT消息時,使用雙緩沖繪制去繪制截圖窗口上要繪制的內(nèi)容。所謂雙緩沖繪圖的思想是,先將所有需要繪制的內(nèi)容繪制到內(nèi)存DC上,這些繪制可能需要時間,然后再將內(nèi)存DC中的內(nèi)容繪制到窗口(DC)上。雙緩沖繪圖是解決繪制時窗口閃爍的有效方法。
在處理WM_PAINT消息時,需要調(diào)用BeginPaint和EndPaint在繪制完窗口后將窗口的無效區(qū)域清空,切記要記得調(diào)用這兩個函數(shù)。如果不調(diào)用這兩個接口,會導(dǎo)致窗口一直有無效區(qū)域,這樣系統(tǒng)一直都檢測到窗口有無效區(qū)域,一直在不斷地產(chǎn)生WM_PAINT消息,這樣程序一直在忙于處理WM_PAINT消息,導(dǎo)致低優(yōu)先的WM_TIMER消息被淹沒被丟棄,界面由于在不斷繪制會產(chǎn)生嚴(yán)重的閃爍問題。在我們的OnPaint函數(shù)中,我們使用到了CPaintDC類,該類中封裝了對BeginPaint和EndPaint的調(diào)用:
CPaintDC::CPaintDC(CWnd* pWnd) { ASSERT_VALID(pWnd); ASSERT(::IsWindow(pWnd->m_hWnd)); if (!Attach(::BeginPaint(m_hWnd = pWnd->m_hWnd, &m_ps))) AfxThrowResourceException(); } CPaintDC::~CPaintDC() { ASSERT(m_hDC != NULL); ASSERT(::IsWindow(m_hWnd)); ::EndPaint(m_hWnd, &m_ps); Detach(); }
有時我們在某些操作后,我們想讓窗口立即刷新,可以組合調(diào)用InvalidateRect和UpdateWindow,InvalidateRect是讓窗口無效,UpdateWindow是讓系統(tǒng)立即產(chǎn)生WM_PAINT消息,并將WM_PAINT投遞到窗口過程(不是將WM_PAINT放到消息隊(duì)列中等待處理),這樣窗口能立即刷新。調(diào)用UpdateWindow就相當(dāng)于讓窗口立即強(qiáng)制刷新。
至于WM_PAINT、BeginPaint、InvalidateRect和UpdateWindow之間的關(guān)系,可以參見我之前專門寫的一篇主題文章:https://blog.csdn.net/chenlycly/article/details/120931704,里面有詳細(xì)地講述這些對象的關(guān)系。
有多種退出截圖的場景,不同的退出場景可能需要有不同的后續(xù)處理,所以我們定義了多種退出截圖時的類型:
enum EmQuitType { emQuitInvalid = -1, // 無效退出類型 emESCQuit = 0, // 按ESC鍵退出 emRClickQuit, // 右鍵單擊退出 emLDClickQuit, // 左鍵雙擊退出 emSendtoBlogQuit, // 發(fā)送到微博退出 emSaveQuit, // 保存截圖后退出 emCancelQuit, // 取消截圖退出 emCompleteQuit, // 完成截圖退出 emMemoryLackQuit, // 內(nèi)存不足引起的gdi操作失敗退出 emCutRectEmptyQuit // 截取區(qū)域?yàn)榭胀顺?nbsp; };
1)按下ESC鍵退出、右鍵點(diǎn)擊退出、保存圖片退出、點(diǎn)擊取消按鈕退出、截取區(qū)域?yàn)榭胀顺?/strong>
這些場景下退出截圖,截圖模塊不需要任何處理,都是單純的退出截圖。
2)雙擊截圖區(qū)域退出截圖、點(diǎn)擊完成按鈕退出截圖
這些場景下,在退出截圖之前,會將截取區(qū)域的圖片位圖保存到剪切板中,同時將截圖保存到磁盤文件中。退出截圖后,如果是聊天框中的截圖入口觸發(fā)的,需要將截取的圖片自動插入到聊天框中。
3)內(nèi)存不足截圖失敗退出
這種場景是因?yàn)橄到y(tǒng)內(nèi)存不足導(dǎo)致GDI函數(shù)調(diào)用失敗,外部需要彈出“截圖失敗,可能是系統(tǒng)內(nèi)存不足引起的,退出部分程序后再試”的提示。
所以我們根據(jù)這些退出的場景設(shè)計(jì)了對應(yīng)的退出類型,在退出截圖時設(shè)置退出類型,并提供獲取退出截圖時退出類型的接口GetQuitType,這樣在退出截圖后,外部調(diào)用GetQuitType獲取當(dāng)前截圖退出的類型,看是否需要進(jìn)行后續(xù)的處理。
最開始我們再代碼中創(chuàng)建位圖時調(diào)用的是CreateCompatibleBitmap,但是該接口在系統(tǒng)內(nèi)存不是很充足的時候會經(jīng)常返回失敗,在日常的測試中經(jīng)常遇到。通過GetLastError獲取到CreateCompatibleBitmap調(diào)用失敗后的錯誤碼是8:
該錯誤碼的描述如上,意思就是當(dāng)前系統(tǒng)的可用內(nèi)存空間不多了,而調(diào)用CreateCompatibleBitmap創(chuàng)建位圖時需要申請一定的內(nèi)存空間,空間不夠時該函數(shù)就會返回失敗了。
經(jīng)后來查閱相關(guān)資料得知,袁峰老師在他編寫的《Windows圖形編程》一書中提過,CreateCompatibleBitmap創(chuàng)建的文圖是DDB位圖,是依賴設(shè)備的設(shè)備相關(guān)位圖,是從內(nèi)核地址空間中分配的,而內(nèi)核內(nèi)存資源比較有限,建議使用CreateDIBSection來創(chuàng)建位圖,書中的具體描述如下:
CreateDIBSection創(chuàng)建的位圖是DIB位圖,是不依賴于設(shè)備的設(shè)備無關(guān)位圖,是從用戶態(tài)地址空間中的虛擬內(nèi)存中分配的,限制比較少,一般都會成功。所以后來我們封裝了一個創(chuàng)建位圖的接口,如下:
// 創(chuàng)建設(shè)備無關(guān)位圖,解決調(diào)用CreateCompatibleBitmap API函數(shù)因內(nèi)存不足創(chuàng)建位圖 // 失敗的問題 HBITMAP CreateDIBBitmap( const int nWidth, const int nHeight ) { BITMAPINFO bmi; ::ZeroMemory( &bmi, sizeof(bmi) ); bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader); bmi.bmiHeader.biWidth = nWidth; bmi.bmiHeader.biHeight = nHeight; bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32; bmi.bmiHeader.biCompression = BI_RGB; bmi.bmiHeader.biSizeImage = nWidth * nHeight * 4;//4=bmi.bmiHeader.biBitCount/8 void* pvBits = NULL; return ::CreateDIBSection( NULL, &bmi, DIB_RGB_COLORS, &pvBits, NULL, 0 ); }
本文講述了屏幕截圖中的一些實(shí)現(xiàn)思路與細(xì)節(jié),但在實(shí)際實(shí)現(xiàn)時的細(xì)節(jié)比上面說的多的多!
此處,我們提供一個工程級、高質(zhì)量的完整屏幕截圖的C++實(shí)現(xiàn)源碼下載鏈接:ScreenCatch.zip
在源碼中,我們將截圖模塊封裝成一個dll,并提供了一個調(diào)用dll接口的工程TestScreenCatch(該工程和截圖dll均提供完整的C++源碼),調(diào)用截圖dll接口的代碼如下:
void CTestScreenCatchDlg::OnBnClickedBtnStartCapture() { CString strPath = GetModuleFullPath(); // 該接口中會彈出截圖的模態(tài)框,截圖對話框關(guān)閉后該接口才會返回 // 接口彈出模塊框,不會堵塞整個線程,模態(tài)框內(nèi)部會接管消息循環(huán),會分發(fā)消息 DoScreenCatch( (LPCTSTR)strPath ); EmQuitType emQuitType = GetQuitType(); if ( emQuitType == emLDClickQuit || emQuitType == emCompleteQuit ) { if ( IsPicFileSaved() ) { TCHAR achPciPath[MAX_PATH] = { 0 }; GetPicFileSavedPath( achPciPath, sizeof(achPciPath)/sizeof(TCHAR) ); CString strTip; strTip.Format( _T("截圖保存到路徑:%s"), achPciPath ); AfxMessageBox( strTip ); } } else if ( emQuitType == emMemoryLackQuit ) { AfxMessageBox( _T("截圖失敗,可能是內(nèi)存不足引起的,退出部分程序后再試!") ); } }
看完上述內(nèi)容是否對您有幫助呢?如果還想對相關(guān)知識有進(jìn)一步的了解或閱讀更多相關(guān)文章,請關(guān)注億速云行業(yè)資訊頻道,感謝您對億速云的支持。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。