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

点击蓝字

208996467e61580b60d97fdf4c4bf942.png

关注我们

1、概述

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

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

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

71a8550220fcc906c0bad922ae6eb6ef.jpeg

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

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

那屏幕截图的主体实现思路到底是什么样子的呢?下面我们就来简单地描述一下。我们实现的一套屏幕截图的效果如下

86b241ebef0695f05533eef3417c6056.jpeg

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

3.1、截图主窗口全屏置顶

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

3.2、桌面灰化

657ef44b21d84a8e7018e9cc0ea40a21.png

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

3.3、窗口自动套索

2593595acf6d79d80fceed3d5ebb62b6.png

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

3.4、区域放大

cd4888ef949510905ed23aac4e2be2a2.jpeg

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

3.5、截取区域的选择

5ca2e62ab66af2d1774c5c48104c0d10.png

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

3.6、截图工具条

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

3.7、矩形等图元的绘制

a87a395a0fadfd2ec5545871a382072e.jpeg

  我们需要设计图元类型对应的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小于bottomLONG 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_hDarkBitmapBITMAP 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:
// ConstructorsCCatchTracker();CCatchTracker(LPCRECT lpSrcRect, UINT nStyle);// Style Flagsenum 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//};// Operationsvoid 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;// Overridablesvirtual 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 helpersint 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:// AttributesUINT m_nStyle;          // current stateCRect m_rect;           // current position (always in pixels)CSize m_sizeMin;        // minimum X and Y size during track operationint m_nHandleSize;      // size of resize handles (default from WIN.INI)BOOL m_bAllowInvert;    // flag passed to Track or TrackRubberBandCRect m_rectLast;CSize m_sizeLast;BOOL m_bErase;          // TRUE if DrawTrackerRect is called for erasingBOOL m_bFinalErase;     // TRUE if DragTrackerRect called for final eraseCOLORREF m_rectColor;   // 当前矩形颜色HWND m_hMsgWnd;         // 向界面发送消息的窗口句柄BOOL m_bMovable;        // 标记该矩形tracker是否可以移动,当点击截图工具条中的按钮后即不可移动
};#endif

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

switch (msg.message){// handle movement/accept messagescase 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 caseif (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就相当于让窗口立即强制刷新。


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:

a3469530d513067d85e37af676ba936e.png

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

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

ef40eaed2cffb191f87ef8e9d93e8411.png

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/8void* pvBits = NULL;return ::CreateDIBSection( NULL, &bmi, DIB_RGB_COLORS, &pvBits, NULL, 0 );
}

12、最后

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

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

d1221de53e84e3b7f6029de77402698a.gif

如果你年满18周岁以上,又觉得学【C语言】太难?想尝试其他编程语言,那么我推荐你学Python,现有价值499元Python零基础课程限时免费领取,限10个名额!
▲扫描二维码-免费领取

dd199544c6d7f2b8fa46140888f1b3f3.gif

戳“阅读原文”我们一起进步

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/337386.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何判断exe文件是debug还是release编译生成的

如何判断exe文件是debug还是release编译生成的结论&#xff1a; 用IDA工具打开exe&#xff0c;然后看Imports里面的依赖库是否有带d或D结尾的&#xff0c;如果有就说明是Debug的 实验&#xff1a;&#xff08;实验环境 vs2017&#xff0c; IDA工具&#xff09; &#xff08;0&…

大屏可视化分配率是什么意思_什么是分配率?

大屏可视化分配率是什么意思诸如“不可持续的分配率”和“您需要保持较低的分配率”之类的短语似乎仅属于Java Champions的词汇表。 复杂&#xff0c;恐怖并被魔术光环包围。 经常发生的情况是&#xff0c;当您更仔细地查看概念时&#xff0c;魔术会随着抽烟消失。 这篇文章试…

C/C++语言动态开辟的杨辉三角

点击蓝字关注我们问题引入杨辉三角相必大家并不陌生&#xff0c;第1行有1列、第二行有2列…第n行有n列&#xff0c;且每行行首和行尾的值都为1&#xff0c;其余的值为上一行两数相加我们在C语言阶段&#xff0c;第一次碰到的杨辉三角应该都是用常规的二维数组存储&#xff0c;可…

git gui 历史版本_这些Git命令都不会,还是不要去面试了

前言以下&#xff0c;项目中经常使用的Git命令&#xff0c;汇总到这里以便与你能快速的学习和掌握Git命令&#xff0c;在文章最后有惊喜哟&#xff0c;一定要看到最后啊&#xff01;使用的 Git版本&#xff1a;git version 2.24.0命令git log# 输出概要日志,这条命令等同于# gi…

java restful_Java EE中的RESTful计时器

java restful在这篇文章中...。 EJB计时器旋风之旅 通过带有示例实现的简单REST接口即时使用EJB计时器 更新&#xff08;2015年7月14日&#xff09; 该应用程序的前端现在可以在OpenShift上使用 。 由于我是前端新手&#xff0c;因此我在其他来源的帮助下组装了此HTML5 Ang…

c# 联合halcon 基于相关性 模板匹配_机器视觉之halcon入门(5)-字符识别exe生成...

2.3.2 第二个halcon程序转EXE程序&#xff1a;字符识别老规矩&#xff0c;每一段halcon代码得用C#二次开发下。根据上一节所教的&#xff0c;我们配置下C#的环境&#xff0c;顺便添加好控件&#xff0c;如下图(2-3-2-1)。图 2-3-2-1控件基本跟上一节一样&#xff0c;只是少了一…

C语言数据的存储和取出(超详细讲解)

点击蓝字关注我们整形的储存我们知道一个整形的存储是以补码的形式储存取出是原码的形式。比如&#xff1a;int a 5;的二进制是101那它的原码应该是&#xff1a;00000000 00000000 00000000 00000101正数的原反补相同那它存进去和取出来都是&#xff1a;00000000 00000000 000…

打印pdf就一页_PDF 文件转换工具

是将 PDF 文件转换为完全可编辑的 Windows 文档最好的转换软件。无论您需要您的内容是 Microsoft Word、Excel、PowerPoint、HTML 还是仅需要文本&#xff0c; 总会给您一个简单的方法&#xff0c;快捷地获取您要的内容。可转换整个文档或选择内容。亦可创建 PDF 文件。PDF 转换…

C++类的this指针,静态成员,友元函数友元类

点击蓝字关注我们1. this指针在上篇讲C中类&#xff0c;对象&#xff0c;封装&#xff0c;继承&#xff08;派生&#xff09;&#xff0c;多态的时候&#xff0c;this指针出现在成员函数中&#xff0c;并使用->成员提取符操作成员变量。在 C 中&#xff0c;每一个对象都能通…

批量提取文件创建时间_批量采集新浪微博用户内容

有时我们需要把某些用户的微博数据全部采集下来用作分析&#xff0c;每条信息复制的工作量是非常低效的&#xff0c;必须要借助工具。今天给大家介绍一款采集软件&#xff1a;微风采集器。打开软件&#xff0c;选择模板&#xff0c;下拉框选&#xff1a;批量提取指定用户微博内…

C++异常的规则

点击蓝字关注我们异常是指存在于程序运行时的异常行为&#xff0c;这些行为超出了函数正常功能的范围&#xff0c;当程序的某部分检测到一个无法处理的问题时&#xff0c;就需要用到异常处理。1. C语言中传统的处理错误方式终止程序&#xff1a;如assert&#xff0c;当发生错误…

异质性查询需要为连线设定_振奋人心!华东理工大学开发新型的荧光染料,为细胞成像奠定基础...

结合并激活荧光染料的适体荧光RNA(FR)已用于对丰富的细胞RNA种类进行成像。然而&#xff0c;诸如低亮度和具有不同光谱特性的染料/适体组合的有限可用性的局限性&#xff0c;限制了这些工具在活的哺乳动物细胞和体内的使用。最近&#xff0c;华东理工大学朱麟勇及杨弋共同通讯在…

C++ STL详解(1)

点击蓝字关注我们概述STL 是“Standard Template Library”的缩写&#xff0c;中文译为“标准模板库”。STL 是 C 标准库的一部分&#xff0c;不用单独安装。C 对模板&#xff08;Template&#xff09;支持得很好&#xff0c;STL 就是借助模板把常用的数据结构及其算法都实现了…

各种说明方法的答题格式_高中化学:选择题答题方法与知识点总结,让你轻松秒杀各种难题...

选择题是化学考试中被广泛采用的一种题型。它具有知识容量大&#xff0c;覆盖面广&#xff0c;构思新颖、灵活巧妙&#xff0c;考试的客观性强&#xff0c;答题简单&#xff0c;评分容易、客观准确等优点。 选择题按考查形式可分为三种类型&#xff0c;即&#xff1a;常规型选择…

C++ STL详解(2)

点击蓝字关注我们来源自网络&#xff0c;侵删刷题时常用的STLstring之前写过一篇 string 的简介但是不是特别全面&#xff0c;这里再补充说明一下。size()返回字符串中字符的数量#include<iostream> #include<string>using namespace std;int main() {string str …

斐波那契数列的四种实现方式(C语言)

点击蓝字关注我们来源自网络&#xff0c;侵删斐波那契数列是一组第一位和第二位为1&#xff0c;从第三位开始&#xff0c;后一位是前两位和的一组递增数列&#xff0c;像这样的&#xff1a;0、1、1、2、3、5、8、13、21、34、55......今天&#xff0c;我们用四种方式来进行实现…

linux make命令_第一章 1.3Linux下安装Redis

1.3.2 Linux下安装Redis第一步: 去官网下载安装包 ,传送门第二步: 上传到Linux服务器,解压redis的安装包tar -zxvf redis-6.0.8.tar.gz这里我已经解压好了,并且移动到了redis目录下第三步: 安装基本环境yum -y insatll gcc-c这里注意一个问题,Centos下安装的gcc默认版本为4.8.5…

C语言strcpy函数的使用

点击蓝字关注我们strcpy简单使用&#xff1a; #include <stdio.h> #include <string.h>struct Student {int sid;char name[20];int age;} st; //此处分号不可省略int main(void) {struct Student st {1000,"zhangsan",20};printf("%d %s %d\n&…

什么镜头最适合拍风景_为什么您的风景摄影套件中应始终装有远摄镜头

当您考虑风景摄影镜头时&#xff0c;许多摄影师只考虑广角镜头。14-24mm f / 2.8或24-70mm f / 2.8等经典镜头是用于风景的流行光学元件。它们是绝佳的选择但是&#xff0c;也值得在风景摄影套件中使用长焦镜头。为什么要使用长镜头进行风景摄影&#xff1f;您在网上或浏览Inst…

C++ STL详解(3)

点击蓝字关注我们简介set 是 关联容器 的一种&#xff0c;是排序好的集合&#xff08;元素已经进行了排序&#xff09;。set 和 multiset 类似&#xff0c;它和 multiset 的差别在于 set 中不能有重复的元素。multiset 的成员函数 set 中也都有。使用 set 必须包含 #include<…