解决win10系统中截图异常放大的问题

        我们的产品中嵌入了im子系统,其中包括类似QQ的截图功能。之前截图功能一直都是好好的,无论是在XP还是win7、win8中。但是这两天,在公司大boss新用的Surface 3 Pro微软平板上出现了异常,开启截图后截图背景被放大了,导致截图背景(即发起截图时的桌面图片)显示不全了,这明显是有问题的,大boss说了,必须要解决。大boss既然都下达指令了,只能将手头的工作先放放,抓紧研究一下这个问题,尽快找到解决方法。

        正常情况下,截图背景是当前整个桌面的图片,但是在win10中被放大后只能显示一部分桌面,即桌面图片显示不全了。这是什么情况?想到QQ,于是拿过来对比了一下,QQ最新版本也有类似的问题,QQ好像对win10做了一些兼容性处理,但是处理的还是有些问题。QQ都有问题,我们也就似乎有理由向boss说明了,这可能是新的win10 UI新特性引起的,QQ都没有解决,我们可能也不太好解决。但是测试同事就不同意了,大boss关注的问题必须要给个说法,要尽可能的解决掉,谁说QQ没解决或者目前解决不了的问题,我们就可以不解决!于是乎,就只能静下心来研究一下了。

        经测试同事提醒,当前Surface平板上显示比例调整为150%,默认的100%在平台上显示太小了,没法用。显示比例调整页面,由于没能在win10系统中截图,以win7界面为例(本文中的相关截图均以win7为例,可能和win10略有差别或者有所出入,关注者可以自行到win10中查看),如下:

解决win10系统中截图异常放大的问题_第1张图片

在win10中,右键点击桌面,在弹出的右键菜单中点击“显示比例”就能进入如上的显示比例设置页面了。win10以前的系统,都是通知通过改变dpi值来调整显示比例的(可以通过Process Monitor工具检测设置页面设置时改写的注册表项来看出来),win10中则有所不同,在win10的注册表中没有和win7一样的注册表项,用Process Monitor也没检测到DPI相关的注册表项。win10的显示比例设置和win7还有一处不同,win10设置后立即生效,win7则提示用户注销后才能生效。当显示比例设置为100%时,截图没有问题,但是设置到100%以上后,就有被放大的问题了。到底是Win10的什么新特性引起的呢?在网上搜了很久也没找到相关问题的解决办法,可能是win10才开始商用吧,这方面的问题报的比较少,但是无意间找到了一篇很有价值的文章,给了我很大的启发:关于Windows高DPI的一些简单总结,链接为:http://www.cppblog.com/weiym/archive/2014/02/18/205841.aspx

        其中,下面的这段话非常有用:通过 DWM 虚拟化支持的 高DPI方式,这种方式的高DPI支持是通过DWM的缩放实现的, 具体过程是这样的, 比如我们当前系统的DPI是200%, 我们程序运行时,系统会告诉你当前DPI仍然是96(100%), 所以我们程序会仍然按照100%的方式进行绘画, 但是但是系统给我们的坐标是根据DPI缩小过后的(也就是我们对窗口调用GetWindowRect或是通过GetSystemMetrics(SM_CXSCREEN)得到的大小会比实际大小减半) , 当我们画完之后, DWM再对整个窗口进行200% 放大后画到屏幕上, 这样看起来我们的程序就自动支持高DPI了通过在我们的截图模块中添加打印日志,GetWindowRect获取的桌面窗口的宽度和高度,GetSystemMetrics获取的屏幕宽度和高度(都是像素值),在显示比例为200%时获取的值,比在显示比例为100%时要缩小一半,和上面的一段话的描述完全一致。

        一般截图的实现思路是,发起截图时将此时整个桌面抓取下来,然后启动一个全屏的对话框,然后将抓取的桌面图片作为对话框背景贴到对话框上。应该是获取桌面图片的代码出问题了,只获取到了一部分桌面区域图片。那么该如何处理才能将整个桌面的图片都获取到然后“全景”显示出来呢?先来看背景图的相关处理代码。

获取桌面尺寸的代码:

	RECT rcDeskTop;
	HWND hWndDeskTop = ::GetDesktopWindow();
	:;GetWindowRect( hWndDeskTop, &rcDeskTop );
获取屏幕宽度和高度的代码:

	m_xScreen = ::GetSystemMetrics( SM_CXSCREEN );
	m_yScreen = ::GetSystemMetrics( SM_CYSCREEN );
拷贝桌面的代码:

// 拷贝桌面,lpRect 代表选定区域,bSave 标记是否将图片内容保存到剪切板中
HBITMAP CCatchScreenDlg::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资源不能释放,因为函数外部要使用
}
获取桌面图片调用该接口时,传入的就是桌面区域的像素大小。函数内部的具体做法是,先获取桌面DC,然后创建一个与桌面DC兼容的、桌面大小的内存位图(通过位图句柄来记录并引用),然后调用BitBlt将桌面图片拷贝到内存位图上。win10中将显示比例调到大于100%时遇到的问题,应该就是该BitBlt左右的代码引起的。 后来又发现了一个很重要的线索,不管显示比例比如何改变,当前的系统分辨率是不变的,还是之前设置的分辨率:

解决win10系统中截图异常放大的问题_第2张图片

当显示比例设置为100%时屏幕的宽度和高度就是当前系统设置的分辨率,当显示比例大于100%时,比如为N%,DWM虚拟化会将GetWindowRect等获取的像素尺寸缩小到分辨率的1/(N%),所以结合获取的桌面图片区域的比例情况,是不是可以考虑使用当前系统设置的分辨率宽度和高度来获取整张桌面图片,因为分辨率宽度和高度大于当前桌面的像素宽度和高度,所以在向内存中拷贝图片时,需要选用StretchBlt,将图片进行缩放,缩放到桌面的像素高度和宽度。经过试验,这样做确实是可行的,具体的做法是,先调用EnumDisplaySettings获取当前系统设置的分辨率,然后判断屏幕宽度是否小于分辨率,若小于就要换用StretchBlt进行缩放处理:

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

	CString strLog;

	HWND hWndDeskTop = ::GetDesktopWindow();

	//HDC hScrDC = ::CreateDC( _T("DISPLAY"), NULL, NULL, NULL );
	HDC hScrDC = ::GetDC( hWndDeskTop ); // 为屏幕创建设备描述表
	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 );

		::ReleaseDC( hWndDeskTop, 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
	CDirectRect rc = *lpRect;
	rc.Normalize();

	// 获得选定区域坐标
	nX = rc.left;
	nY = rc.top;
	nX2 = rc.right;
	nY2 = rc.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 );
		::ReleaseDC( hWndDeskTop, hScrDC );
		::DeleteDC( hMemDC );
		return NULL;
	}

	::SelectObject( hMemDC, hBitmap ); 	// 把新位图选到内存设备描述表中

	BOOL bRet = FALSE;
	BOOL bProcessed = FALSE;
        // 针对win10 DWM虚拟缩放时的处理
        if ( IsOSWin10() && m_bWin10DpiScaleEnabled )
	{
		DEVMODE curDevMode;
		memset( &curDevMode, 0, sizeof(curDevMode) );
		curDevMode.dmSize = sizeof(DEVMODE);
		BOOL bEnumRet = ::EnumDisplaySettings( NULL, ENUM_CURRENT_SETTINGS, &curDevMode );
		if ( bEnumRet && m_xScreen < curDevMode.dmPelsWidth )
		{
			bProcessed = TRUE;
			::SetStretchBltMode( hMemDC, STRETCH_HALFTONE );
			bRet = ::StretchBlt( hMemDC, 0, 0, nWidth, nHeight, hScrDC, 0, 0, curDevMode.dmPelsWidth, 
				curDevMode.dmPelsHeight, SRCCOPY|CAPTUREBLT );
		}
	}

	if ( !bProcessed )
	{
		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 );
		::ReleaseDC( hWndDeskTop, hScrDC );
		::DeleteDC( hMemDC );
		::DeleteObject( hBitmap );
		return NULL;
	}

	if ( hScrDC != NULL )
	{
		//::DeleteDC( hScrDC );
		::ReleaseDC( hWndDeskTop, hScrDC );
	}

	if ( hMemDC != NULL )
	{
		::DeleteDC( hMemDC );
	}

	return hBitmap; // hBitmap资源不能释放,因为函数外部要使用
}
经测试,可以达到我们预想的效果,桌面的整个区域都显示出来了。但是有个小问题,在调用StrecthBlt时虽然设置了STRETCH_HALFTONE标记,但是还是有一定的失真,显示桌面全貌的图片,相对真实的桌面有点模糊,因为大图片被缩小了。不过相对之前的放大问题,这个应该是可以忍受的。

       上面的代码还有一点需要注意,对于获取系统版本,GetVersionEx API函数在win8和win10上基本上已经被逐步废弃了,如果调用的话,获取的将一直是win7的系统版本。要正确获取系统版本,可以使用RtlGetNtVersionNumbers或者NetWkstaGetInfo函数,具体参见:http://blog.csdn.net/chenlycly/article/details/52881385。

        另外,针对某个进程我们可以将DWM虚拟缩放禁用掉,这样就不会有缩放引起的问题了。具体做法是,右键单击目标进程exe文件或者其快捷方式,点击属性,在弹出的属性窗口中,在“兼容性”标签页中,将“高DPI设置时禁用显示缩放”勾选上就可以了,如下所示:(图片是从win7中截图的,可能和win10中略有不同)

解决win10系统中截图异常放大的问题_第3张图片

当勾选上该选项后,该程序启动后,窗口的大小始终固定为100%时的尺寸,不会随着显示比例的设置而变化。通过该方法,可以将截图放大的问题规避掉,但是对于surface平板来说,是行不通的,因为100%时的窗口显示太小,很难看清,所以需要按上面的办法对截图进行改造。

        有人可能会问,我们能否通过代码去判断当前进程是否禁用了高DPI设置时的缩放呢?答案是肯定的。按经验来看,应该将是否勾选对应的标记保存到注册表中了,可以使用Process Monitor工具监测一下注册表操作。具体的做法是,打开右键属性,在任务管理器中查看属于哪个进程,然后到Process Monitor中设置过滤条件,进程属于资源管理器explorer.exe,注意由于该进程注册表操作很频繁,所以要抓的准确,容易看的出来一点,可以先将上面的属性打开,然后开启捕获,然后快速勾选,然后点击应用,然后到Process Monitor停止捕获,这样看到的捕获结果会少一点,这样更容易找到到底写到哪个位置的注册表中。经检测,是保存到HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers中了,如下所示:

解决win10系统中截图异常放大的问题_第4张图片

注意上图是win7中的截图,win10中的禁用标记和win7不一样,因为我当前机器是win7的,之前没有在win10上截图,所以以win7系统来说明,一定要注意。那么从代码角度如何判断目标进程是否禁用了高DPI时的缩放了呢?其实很简单,可以先获取到目标进程的完整路径,然后到注册表HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers位置中,查看是否有对应的如上图的设置选项,如果有,则说明禁了,如果没有,则没有禁用。至于Porcess Monitor工具如何获取及如何使用请关注者自行百度。

        最后,作为windows开发人员,要学会使用Process Explorer、Process Monitor、API Monitor、Process Hacker、Dependency Walker等工具。至于分析软件异常,需要使用windbg和IDA等反汇编工具。

你可能感兴趣的:(Windows相关,重要文章记录,windows多屏幕开发支持)