功能齐全的屏幕截图C++实现详解

目录

1、概述

2、屏幕截图的主要功能点

3、屏幕截图的主体实现思路

3.1、截图主窗口全屏置顶

3.2、桌面灰化

3.3、窗口自动套索

3.4、区域放大

3.5、截取区域的选择

3.5、截图工具条

3.6、矩形等图元的绘制

4、桌面灰化的实现细节

5、窗口自动套索实现

6、区域放大实现

7、截取区域的选择

8、矩形等图元的绘制

9、截图窗口的绘制机制

10、截图退出类型的详细设计

11、创建位图时将CreateCompatibleBitmap替换成CreateDIBSection

12、最后


       屏幕截图已经成为了所有IM即时通讯软件的必备模块,也是日常办公中使用最频繁的功能之一。今天我们从C++开发的角度,来看看屏幕截图的主要功能点是如何实现的,在此给大家分享一下屏幕截图的诸多实现细节。

开发工具:Visual Studio 2010

开发语言:C++

UI框架:MFC(也可以基于开源的duilib框架,其实在duilib中是调用Windows API)

1、概述

       要使用屏幕截图,其实很容易,装一款聊天软件或者办公软件就可以了,比如QQ、企业微信、钉钉、飞书等。但要开发出类似这些软件的屏幕截图模块,则没那么容易。其实实现屏幕截图的技术并不复杂,主要是在各个细节问题的处理上。

       有人可能会说,我并不需要自己开发这些功能,我可以去搜一些开源的代码,也可以到网上搜一堆关于屏幕截图的文章或下载资源,应该可以找到能用的代码或资源了。我想说的是,你大可以去试一试,很多都只是讲到了一点皮毛,基本没有一个实现了完备的截图功能,没有一个能拿到实际的项目中去使用的。简单的写几句代码,玩玩还可以,离真正商用到项目中,差的太远了!真正项目级的代码,要考虑各种场景和细节,要考虑性能和稳定性,是经过多轮测试锤炼出来的,不是随便写写就能搞出来的!

       本文将结合开发屏幕截图的实际项目经历,详细介绍一下屏幕截图各个主要功能点的实现细节与方法,给大家提供一个借鉴和参考。

2、屏幕截图的主要功能点

       一个具有完备功能的屏幕截图应该包含以上多个功能点,比如桌面灰化、窗口自动套索、区域放大、矩形等多个图元绘制、输入文字等。

3、屏幕截图的主体实现思路

       网上很难找到一篇详细介绍屏幕截图完整功能的实现思路的,那屏幕截图的主体实现思路到底是什么样子的呢?下面我们就来简单地描述一下。我们实现的一套屏幕截图的效果如下(文章末尾处提供C++源码下载):

       下面基于我们实现的屏幕截图,详细介绍一下屏幕截图主要的一些功能点和实现思路。 

3.1、截图主窗口全屏置顶

       我们需要创建一个截图的主窗口,开启截图后将该截图主窗口全屏,覆盖整个屏幕,并且给窗口设置TopMost置顶属性。然后我们后续操作都是在这个全屏置顶的窗口上进行绘图出来的,即截图时截图窗口中看到的所有内容(比如桌面灰化、窗口套索、区域放大、各个图元等)都是绘制上去的!

3.2、桌面灰化

       在开启截图时,先将当前桌面上的图像保存到位图对象中,保存两份位图,一份是亮色的桌面图像,一份是经过灰化后的桌面图像。先将灰化的位图绘制到截图对话框上,实现灰化的遮罩。然后根据用户拉动鼠标选择的区域,从亮色位图中抠出对应区域的亮色图像绘制到对话框上,就能达到区域选择的效果了。

3.3、窗口自动套索

       在启动截图时,需要遍历当前系统中所有打开的窗口,以及这些窗口中的子窗口,把这些窗口的坐标位置记录下来保存到内存中。当鼠标移动时,看鼠标移动到哪个最上层的窗口,然后在该窗口的区域绘制上套索的边界,并将该窗口区域“亮”起来。亮起来其实很简单,根据该窗口的坐标到内存中保存的亮色位图中将对应的区域抠出来,绘制到窗口上,然后再在窗口边界上绘制出套索边界线即可。

3.4、区域放大

       其实实现这个功能并不难,可以仔细观察以下主流IM软件的显示细节,就能找到思路和答案了!区域放大是实时地将鼠标移动到的位置的周围区域放大,放大的区域是以鼠标点为中心的一小片矩形区域,然后将该区域放大4倍,将放大的效果绘制到截图对话框上。

3.5、截取区域的选择

       可以使用微软MFC库中提供的橡皮筋类CRectTracker来实现区域的选择。该橡皮筋类对应一个选择边框,通过拉动鼠标,绘制出选择区域的橡皮筋边框,橡皮筋边框支持拖动,改变橡皮筋边框的大小。根据橡皮经选择的区域,到内存中保存的亮色位图中抠出亮色选择区域,绘制到截图窗口上就好了。

3.5、截图工具条

        截图工具条一般做成一个紧贴截图选择区域的窗口,窗口中包含一排功能按钮,一般包括矩形工具、椭圆工具、带箭头直线工具、曲线工具、Undo工具、关闭截图、完成截图这几个功能按钮。选择矩形工具、椭圆工具、带箭头直线工具和曲线工具这四个按钮后,鼠标在截图窗口上绘制的就是对应类型的图元。Undo按钮是回撤上一次绘制的图元。

3.6、矩形等图元的绘制

       我们需要设计图元类型对应的C++类,这些类统一继承于一个CSharp的基类,基类中保存当前绘制图元的线条颜色、起点和终点坐标,还有一个用于绘制图元内容的纯虚接口Draw,具体的Draw操作都在具体的图元中实现。这其实使用C++中多态的概念。

        对于矩形、椭圆和带箭头的直线,我们只需要记录图元的起点和终点坐标就可以了,对于曲线,则由多个直线线段构成的,我们要记录绘制过程中的多个点。当用户左键按下时开始绘制图元,记录此时图元起点坐标,在左键弹起时截图当前图元的绘制,记录图元的终点坐标,然后创建对应类型的图元对象,将起点及终点坐标保存到对象中,然后把这些图元对象保存到图元列表中。窗口需要刷新时,调用列表中这些图像的Draw接口将所有图元绘制到截图窗口上。

4、桌面灰化的实现细节

       开启截图时,将桌面的图像保存到亮色位图对象中,同时对图像进行灰化处理,将处理后的图像保存到暗色位图对象中。保存桌面图像的代码如下所示:

// 拷贝桌面,lpRect 代表选定区域,bSave 标记是否将图片内容保存到剪切板中
HBITMAP CScreenCatchDlg::CopyScreenToBitmap( LPRECT lpRect ) 
{                           
    // 确保选定区域不为空矩形
    if ( IsRectEmpty( lpRect ) )
    {
        return NULL;
    }
    
    CString strLog;

    // 为屏幕创建设备描述表
    HDC hScrDC = ::CreateDC( _T("DISPLAY"), NULL, NULL, NULL ); 
    if ( hScrDC == NULL )
    {
        strLog.Format( _T("[CCatchScreenDlg::CopyScreenToBitmap] 创建DISPLAY失败, GetLastError: %d"), 
            GetLastError() );
        WriteScreenCatchLog( strLog );

        return NULL;
    }

    // 为屏幕设备描述表创建兼容的内存设备描述表
    HDC hMemDC = ::CreateCompatibleDC( hScrDC ); 
    if ( hMemDC == NULL )
    {
        strLog.Format( _T("[CCatchScreenDlg::CopyScreenToBitmap]创建与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;
    }
    
    // 获得选定区域坐标
    nX = lpRect->left;
    nY = lpRect->top;
    nX2 = lpRect->right;
    nY2 = lpRect->bottom;
    
    // 确保选定区域是可见的
    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;
    // 创建一个与屏幕设备描述表兼容的位图
    HBITMAP hBitmap = ::CreateCompatibleBitmap( hScrDC, nWidth, nHeight ); 
    if ( hBitmap == NULL )
    {
        strLog.Format( _T("[CCatchScreenDlg::CopyScreenToBitmap]创建与hScrDC兼容的Bitmap失败, GetLastError: %d"), 
            GetLastError() );
        WriteScreenCatchLog( strLog );

        ::DeleteDC( hScrDC );
        ::DeleteDC( hMemDC );
        return NULL;
    }
    // 把新位图选到内存设备描述表中
    ::SelectObject( hMemDC, hBitmap );     
    
    BOOL bRet = ::BitBlt( hMemDC, 0, 0, nWidth, nHeight, hScrDC, nX, nY, SRCCOPY | CAPTUREBLT );  // CAPTUREBLT - 该参数保证能够截到透明窗口
    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资源不能释放,因为函数外部要使用
}

        如何将桌面图像进行灰化处理呢?其实很简单,只要将保存的桌面位图中的每个像素值的RGB读出来,将每个像素中的R、G、B值都乘以一个系数,然后再将这些值设置回位图中即可,相关代码如下:

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位图资源需要保存在内存中,如不分离,则当对象消亡时,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字节的内存失败,直接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 );
    }

    // 将图像中的所有像素点的RGB值都乘以0.4,即实现了图像的灰化
    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); 
    } 

    // 如果函数成功,那么返回值就是复制的扫描线数;如果函数失败,那么返回值是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 );
}

        内存中要保留两份位图,一份是亮色的桌面图像,一份是经过灰化后的桌面图像。先将灰化的位图绘制到截图对话框上,实现灰化的遮罩。然后根据用户拉动鼠标选择的区域,从亮色位图中抠出对应区域的亮色图像绘制到对话框上,就能达到区域选择的效果了。

5、窗口自动套索实现

       在启动截图时,需要遍历当前系统中所有打开的窗口,以及这些窗口中的子窗口,把这些窗口的坐标位置记录下来保存到内存中。先调用系统API函数EnumWindows,将系统中打开的窗口都枚举出来:

	// 使用EnumWindows来枚举当前系统打开的所有大窗口
	::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;

		 将桌面区域过滤掉
		//if ( !_tcscmp( tWndInfo.m_strWndName, _T("Program Manager") ) )
		//{
		//	return TRUE;
		//}

		::GetWindowRect( hWnd, &(tWndInfo.m_rcWnd) );
		m_listWindows.push_back( tWndInfo );
	}

	return TRUE;
}

然后再遍历这些窗口,使用递归调用的方式找出这些主窗口的各个子窗口,记录下这些子窗口的信息。

       当鼠标移动时,根据鼠标的位置坐标,到窗口信息列表中去遍历,看鼠标移动到哪个最上层的窗口,然后在该窗口的区域绘制上套索的边界,并将该窗口区域“亮”起来。亮起来其实很简单,根据该窗口的坐标到内存中保存的亮色位图中将对应的区域抠出来,绘制到窗口上,然后再在窗口边界上绘制出套索边界线即可。

6、区域放大实现

        实现这点也不难,可以仔细观察以下主流IM软件的显示细节,就能找到思路与方法了!区域放大是实时地将鼠标移动到的位置的周围区域放大,放大的区域是以鼠标点为中心的一小片矩形区域。

       确定待放大区域的坐标后,从内存中保存的桌面亮色位图中抠出亮色的待放大区域,然后调用StretchBlt将放大后的图像绘制到截图窗口上,相关代码如下:

// 在内存pMemDC中绘制自动套索窗口
void CScreenCatchDlg::DrawAutoLassoWndArea( CDC* pMemDC, CDC* pLightDC )
{
	if ( pMemDC == NULL || pLightDC == NULL )
	{
		return;
	}

	if ( m_rcTargetWnd.IsRectEmpty() )
	{
		return;
	}

	// 先从亮色图片将目标窗口抠出
	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;

	// 再在目标窗口周边画上自动套索边界线
    CPen pen( PS_SOLID, 1, RGB( 0, 174, 255 ) );
	CPen* pOldPen = pMemDC->SelectObject( &pen );
	CBrush* pOldBrush = ( CBrush* )pMemDC->SelectStockObject( NULL_BRUSH ); // 使用NULL_BRUSH调用SelectStockObject可以实现透明画刷的效果
	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、截取区域的选择

        微软MFC库中的橡皮筋类CRectTracker是个好东西,绘制出来的是个有边框线的矩形边界线,边框线上有八个点可以用鼠标点击拖动来改变矩形边界线的大小。我们正可以使用这个橡皮筋类来实现截图区域的选择。

       橡皮筋类CRectTracker实现的有点复杂,也很巧妙,我们将该类的代码从MFC库中拿出来,对其进行一些简单灵活的改造,就可以用到截图模块中。添加一些消息通知和额外的处理机制。抠出来的类,我们命名为CCatchTracker,其头文件如下所示:

/
// CCatchTracker - simple rectangular tracking rectangle w/resize handles


// CCatchTracker类从MFC源文件COPY过来,根据自身的需要做了修改,对消息机制
// 做了点改动,增加了部分接口


#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 ) // 更新截图工具条位置消息,当截取区域发生变化时要向界面发送该消息

#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 //设置中间
	};

// 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:
	// 设置调整光标
	void SetResizeCursor(UINT nID_N_S,UINT nID_W_E,UINT nID_NW_SE, UINT nID_NE_SW,UINT nIDMiddle);
	// 创建画刷,内部调用
	void CreatePen();
	// 设置矩形颜色
	void SetRectColor(COLORREF rectColor);
	// 设置该矩形tracker是否可以移动,当点击截图工具条中的按钮后即不可移动
	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;   // 当前矩形颜色
    HWND m_hMsgWnd;         // 向界面发送消息的窗口句柄
    BOOL m_bMovable;        // 标记该矩形tracker是否可以移动,当点击截图工具条中的按钮后即不可移动
};

#endif

       根据橡皮经选择的区域,到内存中保存的亮色位图中抠出亮色选择区域,绘制到截图窗口上就好了。截图工具条是紧贴着橡皮筋选择区域的,位于该区域的下方,当橡皮筋区域大小发生变化时,要通知截图工具条窗口跟着截图区域一起动,使截图工具条紧跟着橡皮筋选择区域。所以我们在橡皮筋类中抛出如下的通知消息:

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

			// 发送矩形区域的左上角和右下角的坐标给界面,一方面在移动矩形时要用到,
			// 一方面在更新界面中的截图工具条的位置时要用到
			if ( IsWindow( m_hMsgWnd ) ) // 检验是否是有效的窗口句柄
			{
				BOOL bLBtnUp = FALSE;
				if ( msg.message == WM_LBUTTONUP )
				{
					bLBtnUp = TRUE;
				}
				::SendMessage(m_hMsgWnd, WM_UPDATE_TOOLBAR_POS, (WPARAM)&m_rect, (LPARAM)bLBtnUp );
			}

8、矩形等图元的绘制

        截图中要支持矩形、椭圆、带箭头直线和曲线四种图元的绘制,我们分别设计了与图元类型对应的C++类,这些类统一继承于一个CSharp的基类,基类中保存当前绘制图元的线条颜色、起点和终点坐标,还有一个用于绘制图元内容的纯虚接口Draw:

// 形状基类
class CShape
{
public:
	CShape();
	virtual ~CShape();

	virtual void Draw( CDC* pDC ) = 0;

protected:
	CPoint m_startPt;  // 起点
	CPoint m_endPt;    // 终点
    COLORREF m_color;  // 当前使用颜色
};

具体的Draw操作都在具体的图元中实现。这其实使用C++中多态的概念。

       以矩形图元为例,矩形类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() );
}

        对于矩形、椭圆和带箭头的直线,我们只需要记录图元的起点和终点坐标就可以了,对于曲线,则由多个直线线段构成的,我们要记录绘制过程中的多个点。当用户左键按下时开始绘制图元,记录此时图元起点坐标,在左键弹起时截图当前图元的绘制,记录图元的终点坐标,然后创建对应类型的图元对象,将起点及终点坐标保存到对象中,然后把这些图元对象保存到图元列表中。窗口需要刷新时,调用列表中这些图像的Draw接口将所有图元绘制到截图窗口上。

       最开始我们是使用GDI函数绘制图元的,比如GDI中的API函数Reactangle(绘制矩形)、Ellipse(绘制椭圆)等,但在绘制带箭头的直线和曲线时,GDI函数绘制出来的结果中有明显的锯齿,效果很不好。所以后来我们将图元的绘制全部改成使用GDI+库来处理,GDI+中的Graphics类在绘制图元时,可以设置反锯齿的模式:

	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、截图窗口的绘制机制

       整个全屏置顶的截图主窗口上面显示的所有内容都是都是我们在截图窗口中绘制出来的,比如窗口的自动套索效果、区域放大效果、截图区域的橡皮筋选择框、各种图元的绘制等。

        我们要在截图窗口上接管所有内容的绘制,需要拦截截图窗口的WM_ERASEBKGND和WM_PAINT消息。首先在收到WM_ERASEBKGND消息后,直接return TRUE,不需要系统帮我们绘制背景:

BOOL CScreenCatchDlg::OnEraseBkgnd( CDC* pDC ) 
{
	return TRUE;
}

       在收到WM_PAINT消息时,使用双缓冲绘制去绘制截图窗口上要绘制的内容。所谓双缓冲绘图的思想是,先将所有需要绘制的内容绘制到内存DC上,这些绘制可能需要时间,然后再将内存DC中的内容绘制到窗口(DC)上。双缓冲绘图是解决绘制时窗口闪烁的有效方法。

       在处理WM_PAINT消息时,需要调用BeginPaint和EndPaint在绘制完窗口后将窗口的无效区域清空,切记要记得调用这两个函数。如果不调用这两个接口,会导致窗口一直有无效区域,这样系统一直都检测到窗口有无效区域,一直在不断地产生WM_PAINT消息,这样程序一直在忙于处理WM_PAINT消息,导致低优先的WM_TIMER消息被淹没被丢弃,界面由于在不断绘制会产生严重的闪烁问题。在我们的OnPaint函数中,我们使用到了CPaintDC类,该类中封装了对BeginPaint和EndPaint的调用:

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

        有时我们在某些操作后,我们想让窗口立即刷新,可以组合调用InvalidateRect和UpdateWindow,InvalidateRect是让窗口无效,UpdateWindow是让系统立即产生WM_PAINT消息,并将WM_PAINT投递到窗口过程(不是将WM_PAINT放到消息队列中等待处理),这样窗口能立即刷新。调用UpdateWindow就相当于让窗口立即强制刷新。
       至于WM_PAINT、BeginPaint、InvalidateRect和UpdateWindow之间的关系,可以参见我之前专门写的一篇主题文章:https://blog.csdn.net/chenlycly/article/details/120931704,里面有详细地讲述这些对象的关系。

10、截图退出类型的详细设计

       有多种退出截图的场景,不同的退出场景可能需要有不同的后续处理,所以我们定义了多种退出截图时的类型:

enum EmQuitType
{
	emQuitInvalid = -1,     // 无效退出类型
	emESCQuit   = 0,        // 按ESC键退出
	emRClickQuit,           // 右键单击退出
	emLDClickQuit,          // 左键双击退出
	emSendtoBlogQuit,       // 发送到微博退出
	emSaveQuit,             // 保存截图后退出
	emCancelQuit,           // 取消截图退出
	emCompleteQuit,         // 完成截图退出
	emMemoryLackQuit,       // 内存不足引起的gdi操作失败退出
	emCutRectEmptyQuit      // 截取区域为空退出       
};

1)按下ESC键退出、右键点击退出、保存图片退出、点击取消按钮退出、截取区域为空退出
       这些场景下退出截图,截图模块不需要任何处理,都是单纯的退出截图。

2)双击截图区域退出截图、点击完成按钮退出截图
       这些场景下,在退出截图之前,会将截取区域的图片位图保存到剪切板中,同时将截图保存到磁盘文件中。退出截图后,如果是聊天框中的截图入口触发的,需要将截取的图片自动插入到聊天框中。

3)内存不足截图失败退出

        这种场景是因为系统内存不足导致GDI函数调用失败,外部需要弹出“截图失败,可能是系统内存不足引起的,退出部分程序后再试”的提示。
        所以我们根据这些退出的场景设计了对应的退出类型,在退出截图时设置退出类型,并提供获取退出截图时退出类型的接口GetQuitType,这样在退出截图后,外部调用GetQuitType获取当前截图退出的类型,看是否需要进行后续的处理。

11、创建位图时将CreateCompatibleBitmap替换成CreateDIBSection

       最开始我们再代码中创建位图时调用的是CreateCompatibleBitmap,但是该接口在系统内存不是很充足的时候会经常返回失败,在日常的测试中经常遇到。通过GetLastError获取到CreateCompatibleBitmap调用失败后的错误码是8:

该错误码的描述如上,意思就是当前系统的可用内存空间不多了,而调用CreateCompatibleBitmap创建位图时需要申请一定的内存空间,空间不够时该函数就会返回失败了。

       经后来查阅相关资料得知,袁峰老师在他编写的《Windows图形编程》一书中提过,CreateCompatibleBitmap创建的文图是DDB位图,是依赖设备的设备相关位图,是从内核地址空间中分配的,而内核内存资源比较有限,建议使用CreateDIBSection来创建位图,书中的具体描述如下:

       CreateDIBSection创建的位图是DIB位图,是不依赖于设备的设备无关位图,是从用户态地址空间中的虚拟内存中分配的,限制比较少,一般都会成功。所以后来我们封装了一个创建位图的接口,如下:

// 创建设备无关位图,解决调用CreateCompatibleBitmap API函数因内存不足创建位图
// 失败的问题
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、最后

       本文讲述了屏幕截图中的一些实现思路与细节,但在实际实现时的细节比上面说的多的多!

此处,我们提供一个工程级、高质量的完整屏幕截图的C++实现源码下载链接:功能齐全的屏幕截图C++实现源码 - ScreenCatch.zipicon-default.png?t=LA46https://download.csdn.net/download/chenlycly/38061403

在源码中,我们将截图模块封装成一个dll,并提供了一个调用dll接口的工程TestScreenCatch(该工程和截图dll均提供完整的C++源码),调用截图dll接口的代码如下:

void CTestScreenCatchDlg::OnBnClickedBtnStartCapture()
{
	CString strPath = GetModuleFullPath();

	// 该接口中会弹出截图的模态框,截图对话框关闭后该接口才会返回
	// 接口弹出模块框,不会堵塞整个线程,模态框内部会接管消息循环,会分发消息
	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("截图失败,可能是内存不足引起的,退出部分程序后再试!") );
	}
}

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>