溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何分析C++實(shí)現(xiàn)功能齊全的屏幕截圖示例

發(fā)布時間:2021-11-10 13:38:16 來源:億速云 閱讀:305 作者:柒染 欄目:開發(fā)技術(shù)

如何分析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)

    1、概述

    要使用屏幕截圖,其實(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é)與方法,給大家提供一個借鑒和參考。

    2、屏幕截圖的主要功能點(diǎn)

    如何分析C++實(shí)現(xiàn)功能齊全的屏幕截圖示例

    一個具有完備功能的屏幕截圖應(yīng)該包含以上多個功能點(diǎn),比如桌面灰化、窗口自動套索、區(qū)域放大、矩形等多個圖元繪制、輸入文字等。

    3、屏幕截圖的主體實(shí)現(xiàn)思路

    網(wǎng)上很難找到一篇詳細(xì)介紹屏幕截圖完整功能的實(shí)現(xiàn)思路的,那屏幕截圖的主體實(shí)現(xiàn)思路到底是什么樣子的呢?下面我們就來簡單地描述一下。我們實(shí)現(xiàn)的一套屏幕截圖的效果如下(文章末尾處提供C++源碼下載):

    如何分析C++實(shí)現(xiàn)功能齊全的屏幕截圖示例

    下面基于我們實(shí)現(xiàn)的屏幕截圖,詳細(xì)介紹一下屏幕截圖主要的一些功能點(diǎn)和實(shí)現(xiàn)思路。

    3.1、截圖主窗口全屏置頂

    我們需要創(chuàng)建一個截圖的主窗口,開啟截圖后將該截圖主窗口全屏,覆蓋整個屏幕,并且給窗口設(shè)置TopMost置頂屬性。然后我們后續(xù)操作都是在這個全屏置頂?shù)拇翱谏线M(jìn)行繪圖出來的,即截圖時截圖窗口中看到的所有內(nèi)容(比如桌面灰化、窗口套索、區(qū)域放大、各個圖元等)都是繪制上去的!

    3.2、桌面灰化

    如何分析C++實(shí)現(xiàn)功能齊全的屏幕截圖示例

    在開啟截圖時,先將當(dāng)前桌面上的圖像保存到位圖對象中,保存兩份位圖,一份是亮色的桌面圖像,一份是經(jīng)過灰化后的桌面圖像。先將灰化的位圖繪制到截圖對話框上,實(shí)現(xiàn)灰化的遮罩。然后根據(jù)用戶拉動鼠標(biāo)選擇的區(qū)域,從亮色位圖中摳出對應(yīng)區(qū)域的亮色圖像繪制到對話框上,就能達(dá)到區(qū)域選擇的效果了。

    3.3、窗口自動套索

    如何分析C++實(shí)現(xiàn)功能齊全的屏幕截圖示例

    在啟動截圖時,需要遍歷當(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ū)域摳出來,繪制到窗口上,然后再在窗口邊界上繪制出套索邊界線即可。

    3.4、區(qū)域放大

    如何分析C++實(shí)現(xiàn)功能齊全的屏幕截圖示例

    其實(shí)實(shí)現(xiàn)這個功能并不難,可以仔細(xì)觀察以下主流IM軟件的顯示細(xì)節(jié),就能找到思路和答案了!區(qū)域放大是實(shí)時地將鼠標(biāo)移動到的位置的周圍區(qū)域放大,放大的區(qū)域是以鼠標(biāo)點(diǎn)為中心的一小片矩形區(qū)域,然后將該區(qū)域放大4倍,將放大的效果繪制到截圖對話框上。

    3.5、截取區(qū)域的選擇

    如何分析C++實(shí)現(xiàn)功能齊全的屏幕截圖示例

    可以使用微軟MFC庫中提供的橡皮筋類CRectTracker來實(shí)現(xiàn)區(qū)域的選擇。該橡皮筋類對應(yīng)一個選擇邊框,通過拉動鼠標(biāo),繪制出選擇區(qū)域的橡皮筋邊框,橡皮筋邊框支持拖動,改變橡皮筋邊框的大小。根據(jù)橡皮經(jīng)選擇的區(qū)域,到內(nèi)存中保存的亮色位圖中摳出亮色選擇區(qū)域,繪制到截圖窗口上就好了。

    3.5、截圖工具條

    截圖工具條一般做成一個緊貼截圖選擇區(qū)域的窗口,窗口中包含一排功能按鈕,一般包括矩形工具、橢圓工具、帶箭頭直線工具、曲線工具、Undo工具、關(guān)閉截圖、完成截圖這幾個功能按鈕。選擇矩形工具、橢圓工具、帶箭頭直線工具和曲線工具這四個按鈕后,鼠標(biāo)在截圖窗口上繪制的就是對應(yīng)類型的圖元。Undo按鈕是回撤上一次繪制的圖元。

    3.6、矩形等圖元的繪制

    如何分析C++實(shí)現(xiàn)功能齊全的屏幕截圖示例

    我們需要設(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接口將所有圖元繪制到截圖窗口上。

    4、桌面灰化的實(shí)現(xiàn)細(xì)節(jié)

    開啟截圖時,將桌面的圖像保存到亮色位圖對象中,同時對圖像進(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ū)域選擇的效果了。

    5、窗口自動套索實(shí)現(xiàn)

    在啟動截圖時,需要遍歷當(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ū)域摳出來,繪制到窗口上,然后再在窗口邊界上繪制出套索邊界線即可。

    6、區(qū)域放大實(shí)現(xiàn)

    實(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 );
    }

    7、截取區(qū)域的選擇

    微軟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 );
    			}

    8、矩形等圖元的繪制

    截圖中要支持矩形、橢圓、帶箭頭直線和曲線四種圖元的繪制,我們分別設(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;

    9、截圖窗口的繪制機(jī)制

    整個全屏置頂?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)系。

    10、截圖退出類型的詳細(xì)設(shè)計(jì)

    有多種退出截圖的場景,不同的退出場景可能需要有不同的后續(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ù)的處理。

    11、創(chuàng)建位圖時將CreateCompatibleBitmap替換成CreateDIBSection

    最開始我們再代碼中創(chuàng)建位圖時調(diào)用的是CreateCompatibleBitmap,但是該接口在系統(tǒng)內(nèi)存不是很充足的時候會經(jīng)常返回失敗,在日常的測試中經(jīng)常遇到。通過GetLastError獲取到CreateCompatibleBitmap調(diào)用失敗后的錯誤碼是8:

    如何分析C++實(shí)現(xiàn)功能齊全的屏幕截圖示例

    該錯誤碼的描述如上,意思就是當(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)建位圖,書中的具體描述如下:

    如何分析C++實(shí)現(xiàn)功能齊全的屏幕截圖示例

    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 );
    }

    12、最后

    本文講述了屏幕截圖中的一些實(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è)資訊頻道,感謝您對億速云的支持。

    向AI問一下細(xì)節(jié)

    免責(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)容。

    c++
    AI