C++实现功能齐全的屏幕截图示例(附demo)

目录
  • 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++实现源码下载链接:ScreenCatch.zip

在源码中,我们将截图模块封装成一个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("截图失败,可能是内存不足引起的,退出部分程序后再试!") );
	}
}

到此这篇关于C++实现功能齐全的屏幕截图示例(附demo)的文章就介绍到这了,更多相关C++ 屏幕截图内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++实现屏幕截图

    上回分享了一个全屏截图的代码,保存为BMP,参考:C++实现屏幕截图(全屏截图) 实际使用的过程中我发现截图文件实在大,无奈又整成了PNG截图,现在分享出来. MakePNG.h //MakePNG.h #pragma once #include <GdiPlus.h> using namespace Gdiplus; #pragma comment(lib,"GdiPlus.lib") class CMakePNG { public: CMakePNG(void); ~C

  • C++实现屏幕截图功能

    本文实例为大家分享了C++实现全屏截图功能的具体代码,供大家参考,具体内容如下 最近维护的项目,在某些情况下,光有日志还不行,于是添加了截图功能,特定情况下,会自动截图,辅助分析,从而改进程序.以下是截图实现代码. void CDemoDlg::ScreenShot(void) { CWnd *pDesktop = GetDesktopWindow(); CDC *pdeskdc = pDesktop->GetDC(); CRect re; //获取窗口的大小 pDesktop->GetCli

  • C++实现功能齐全的屏幕截图示例(附demo)

    目录 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.最后

  • Flutter手势密码的实现示例(附demo)

    目录 前言 开始 绘制圆点 绘制手势路径 组合9个圆点盘和手势路径 手势密码组件的使用 上传自定义组件到pub仓库 后记 前言 本篇记录的是使用Flutter完成手势密码的功能,大致效果如下图所示: 该手势密码的功能比较简单,下面会详细记录实现的过程,另外还会简单说明如何将该手势密码作为插件发布到pub仓库. 开始 实现上面的手势密码并不难,大致可以拆分成如下几部分来完成: 绘制9个圆点 绘制手指滑动的线路 合并以上两个部分 绘制圆点 我们使用面向对象的方式来处理9个圆点的绘制,每个圆点作为一个

  • vue项目中扫码支付的实现示例(附demo)

    目录 需求背景 思路分析 UI展示 开始使用 一 编写支付组件模板 二 支付组件的JS相关代码和说明 附:组件JS完整的源码 需求背景 市场报告列表展示的报告有两种类型,一种是免费报告,另一种是付费报告.免费报告用户可以直接查看,付费报告需要用户购买之后才能查看. 思路分析 点击查看为付费报告,弹出支付二维码. 创建订单,二维码进行倒计时,其展示5秒后开始监听支付回调结果,频次为五秒一次. 倒计时第一次倒数到0秒,提醒二维码过期让用户点击刷新二维码. 继续倒计时并开始监听支付回调结果. 刷新之后

  • Python实现病毒仿真器的方法示例(附demo)

    最近新冠在神州大陆横行,全国上下一心抗击疫情.作为一枚程序员,我也希望可以为抗击疫情做出自己的贡献,钟院士一直劝说大家不要出门,减少人口间的流动.对此,我特意做了一个病毒仿真器,探询冠状病毒传播. 1. 仿真效果 仿真开始,一开始只有5个发病者,传播率为0.8,潜伏期为14天 由于人口的流动,以及医院床位的隔离,一开始病毒扩撒不是很速度 随着医院床位满了,隔离失败,加上人口的流动,病患数开始几何式的增加 2. 什么是仿真器 仿真器(emulator)以某一系统复现另一系统的功能.与计算机模拟系统

  • Zend Framework入门之环境配置及第一个Hello World示例(附demo源码下载)

    本文实例讲述了Zend Framework入门之环境配置及第一个Hello World程序.分享给大家供大家参考,具体如下: 第一步:确认你的PHP环境: 1.请PHPer确认你的PHP版本是否在5.2.0以上..如果不是的话..请更新到5.2.0,否则.Zend Framework 好像用不了..我自己有试过. 遇到过这样的问题..所以请你们自己测试一下..PHP源码最新版下载地址为:http://www.php.net/downloads.php. 2.你的PHP环境配置好了之后,请打开ph

  • 微信小程序实现点击按钮移动view标签的位置功能示例【附demo源码下载】

    本文实例讲述了微信小程序实现点击按钮移动view标签的位置功能.分享给大家供大家参考,具体如下: 1.效果展示 2.关键代码 index.wxml文件 <view class="view" style="left:{{viewLeft}}px;">我是view标签</view> <button class="btn" type="default" bindtap="changeLocat

  • 微信小程序实现点击按钮修改view标签背景颜色功能示例【附demo源码下载】

    本文实例讲述了微信小程序实现点击按钮修改view标签背景颜色功能.分享给大家供大家参考,具体如下: 1.效果展示 2.操作步骤: ① 数据绑定view样式背景属性值 ② 通过逻辑文件设置该背景属性初始值 ③ 通过点击按钮修改背景属性值 3.关键代码 index.wxml文件: <view style="background:{{viewBg}};color:white;height:100px;">我是view标签</view> <button type=

  • 微信小程序实现点击按钮修改文字大小功能【附demo源码下载】

    本文实例讲述了微信小程序实现点击按钮修改文字大小功能.分享给大家供大家参考,具体如下: 1.效果展示 2.关键代码 index.wxml文件 <view class="view" style="font-size:{{fontSize}}pt">我是view标签</view> <button class="btn" type="default" bindtap="magnifyFontS

  • c# WPF中通过双击编辑DataGrid中Cell的示例(附源码)

    背景 在很多的时候我们需要编辑DataGrid中每一个Cell,编辑后保存数据,原生的WPF中的DataGrid并没有提供这样的功能,今天通过一个具体的例子来实现这一个功能,在这个例子中DataGrid中的数据类型可能是多种多样的,有枚举.浮点类型.布尔类型.DateTime类型,每一种不同的类型需要双击以后呈现不同的效果,本文通过使用Xceed.Wpf.DataGrid这个动态控件库来实现这个功能,当前使用的Dll版本是2.5.0.0,不同的版本可能实现上面有差别,这个在使用的时候需要特别注意

  • python实现web邮箱扫描的示例(附源码)

    信息收集是进行渗透测试的关键部分,掌握大量的信息对于攻击者来说是一件非常重要的事情,比如,我们知道一个服务器的版本信息,我们就可以利用该服务器框架的相关漏洞对该服务器进行测试.那么如果我们掌握了该服务器的管理员的邮箱地址,我们就可以展开一个钓鱼攻击.所以,对web站点进行邮箱扫描,是进行钓鱼攻击的一种前提条件. 下面,我们利用python脚本来实现一个web站点的邮箱扫描爬取.目的是在实现这个脚本的过程中对python进行学习 最后有完整代码 基本思路 我们向工具传入目标站点之后,首先要对输入进

随机推荐