将WebBrowser中的页面截屏保存为图片

先说一点题外话,将WEB页面渲染成图片有比较好的开源工具,如CutyCapt ,它使用WebKit渲染,兼容多种操作系统,适合于在服务器上作为后台服务运行。


不过,这里说到的是对WebBrowser内的页面进行截图并保存. WebBrowser本质上就是IE内核的浏览器。使用mshtml来渲染页面的话,依赖GDI,所以不可能作为后台服务运行。

获取WebBrowser截屏的方法很多, PrintWindow / IHTMLElementRender / IViewObject。不管使用哪种方法,都需考虑长页面的问题。因为这些方法都只能截屏clientArea区域,也就是说没显示的部分无法截图,必须通过多次截图完成整个页面截图的拼合。


本文使用的是PrintWindow方式,这种方式原理上能够兼容其它所有的浏览器。


首先通过设置ControlSite将浏览器的边框和滚动条隐藏,这样客户区只有WEB页面。

HRESULT FAR EXPORT  CCustomControlSite::XDocHostUIHandler::GetHostInfo( DOCHOSTUIINFO* pInfo )
{
	METHOD_PROLOGUE(CCustomControlSite, DocHostUIHandler)
	
	pInfo->cbSize = sizeof(DOCHOSTUIINFO);
	pInfo->dwFlags = DOCHOSTUIFLAG_DIALOG | 
		DOCHOSTUIFLAG_DISABLE_HELP_MENU |
		DOCHOSTUIFLAG_ACTIVATE_CLIENTHIT_ONLY |
		DOCHOSTUIFLAG_URL_ENCODING_ENABLE_UTF8 |
		DOCHOSTUIFLAG_NO3DOUTERBORDER |
		DOCHOSTUIFLAG_NO3DBORDER |
		DOCHOSTUIFLAG_SCROLL_NO |
		DOCHOSTUIFLAG_USE_WINDOWLESS_SELECTCONTROL;

	pInfo->dwDoubleClick = DOCHOSTUIDBLCLK_DEFAULT;

	return S_OK;
}

然后挂接 document.body.onload事件,保证页面上的图片都加载完成。

void CWebBrowser::OnDocumentComplete( IDispatch *pDisp, VARIANT *URL)
{
	CComQIPtr<IWebBrowser2> pWebBrowser2(pDisp);

	if( pWebBrowser2 )
	{
		CComPtr<IDispatch> pDispatch;
		if( S_OK == pWebBrowser2->get_Document(&pDispatch) )
		{
			CComQIPtr<IHTMLDocument2> pDoc2(pDispatch);
			if( pDoc2 )
			{
				BSTR bstrReadyState;
				if( S_OK == pDoc2->get_readyState(&bstrReadyState) )
				{
					if( 0 == _wcsicmp( bstrReadyState, L"complete") )
					{
						CComPtr<IHTMLWindow2> pWnd2;
						if( S_OK == pDoc2->get_parentWindow(&pWnd2) )
						{
							CComPtr<IHTMLWindow2> pTopWnd2;
							if( S_OK == pWnd2->get_top(&pTopWnd2) )
							{
								CComQIPtr<IHTMLWindow3> pTopWnd3(pTopWnd2);
								if( pTopWnd3 )
								{
									VARIANT_BOOL vbSuccess = VARIANT_FALSE;  

									if( m_pOnPageLoadEvent )
									{
										pTopWnd3->detachEvent( _bstr_t(L"onload"), m_pOnPageLoadEvent);
									}

									m_pOnPageLoadEvent = (CDOMEventHandler*)CDOMEventHandler::CreateEventHandler( &CWebBrowser::OnPageLoad, (LONG_PTR)this);

									pTopWnd3->attachEvent( _bstr_t(L"onload")
										, m_pOnPageLoadEvent
										, &vbSuccess
										);
								}
							}
						}
					}
					SysFreeString(bstrReadyState);
				}
			}// pDoc2
		}// get_Document
	}// pWebBrowser2
	
}


当页面onload后,就可以进行截屏了。下面是关键代码,其实原理很简单:

首先创建一个大小等于 document.body.clientWidth 宽, document.documentElement.scrollHeight 高的 画布。 然后依次滚动页面,每次滚动的距离等于客户区的高度,滚动后截图,依此结束。

void CWebBrowser::CaptureToImage()
{
	
	CComQIPtr<IHTMLDocument2> pDoc2 = this->get_Document();
	CComQIPtr<IHTMLDocument3> pDoc3(pDoc2);
	if( pDoc2 )
	{
		CComPtr<IHTMLElement> pBodyElem;
		CComPtr<IHTMLWindow2> pWnd2, pTopWnd2;
		
		if( S_OK == pDoc2->get_body(&pBodyElem) &&
			S_OK == pDoc2->get_parentWindow(&pWnd2) &&
			S_OK == pWnd2->get_top(&pTopWnd2))
		{	
			long nScrollHeight = 0L, nClientWidth = 0L, nClientHeight = 0L;

			CComPtr<IHTMLElement> pDocElem;
			pDoc3->get_documentElement(&pDocElem);
			CComQIPtr<IHTMLElement2> pDocElem2(pDocElem);

			CComQIPtr<IHTMLElement2> pBodyElem2(pBodyElem);
			pBodyElem2->get_scrollHeight(&nScrollHeight);

			RECT rect;
			GetClientRect(&rect);
			nClientWidth = rect.right - rect.left;
			nClientHeight = rect.bottom - rect.top;
			if( nScrollHeight > 0 && nClientWidth > 0 && nClientHeight > 0 )
			{
				Bitmap bitmap(nClientWidth, nScrollHeight);
				Graphics g(&bitmap);
				HDC hDC = g.GetHDC();
				if (hDC != NULL)
				{
					long nYPos = nScrollHeight - nClientHeight;

					
					do 
					{
						pTopWnd2->scrollTo( 0, nYPos);

						{
							long y1 = 0, y2 = 0;
							pDocElem2->get_scrollTop(&y1);
							pBodyElem2->get_scrollTop(&y2);
							nYPos = max(y1, y2);
						}

						HDC hMemDC = ::CreateCompatibleDC(hDC);
						HBITMAP hBitmap = ::CreateCompatibleBitmap( hDC, nClientWidth, nClientHeight);
						::SelectObject( hMemDC, hBitmap);
						VERIFY(::PrintWindow( GetSafeHwnd(), hMemDC, PW_CLIENTONLY));
						::SelectObject( hMemDC, NULL);

						::BitBlt( hDC, 0, nYPos, nClientWidth, nClientHeight, hMemDC, 0, 0, SRCCOPY);

						::DeleteDC(hMemDC);
						::DeleteObject(hBitmap);
								
						if( nYPos <= 0)
							break;
						nYPos -= nClientHeight;
						if( nYPos < 0 )
							nYPos = 0;
					
						
					} while (true);

						
					g.ReleaseHDC(hDC);

					CLSID pngClsid;  
					GetEncoderClsid(L"image/png", &pngClsid);  

						 
					bitmap.Save(L"L:\\WebSitesMonitoring\\1.png", &pngClsid);
				}
			}
		}
	}
}

最终通过GDI+保存成PNG格式。下面是 GetEncoderClsid方法

int CWebBrowser::GetEncoderClsid(const WCHAR* format, CLSID* pClsid)  
{  
	UINT  num = 0;          // number of image encoders  
	UINT  size = 0;         // size of the image encoder array in bytes  

	ImageCodecInfo* pImageCodecInfo = NULL;  

	GetImageEncodersSize(&num, &size);  
	if(size == 0)  
		return -1;  // Failure  

	pImageCodecInfo = (ImageCodecInfo*)(malloc(size));  
	if(pImageCodecInfo == NULL)  
		return -1;  // Failure  

	GetImageEncoders(num, size, pImageCodecInfo);  

	for(UINT j = 0; j < num; ++j)  
	{  
		if( wcscmp(pImageCodecInfo[j].MimeType, format) == 0 )  
		{  
			*pClsid = pImageCodecInfo[j].Clsid;  
			free(pImageCodecInfo);  
			return j;  // Success  
		}      
	}  

	free(pImageCodecInfo);  
	return -1;  // Failure  
}  

下面是效果,图片已经被CSDN压缩的不成样子了。。



这个错误信息表明,在尝试向数据库插入数据时,提供的日期值 `'12830-08-01 00:00:00'` 超出了 MySQL 中 `DATETIME` 或 `TIMESTAMP` 类型的有效范围。 ### 原因分析 1. **MySQL 的有效日期范围** - 对于 `DATE`, `DATETIME`, 和 `TIMESTAMP` 数据类型,有效的日期范围通常是: ``` 1000-01-01 到 9999-12-31 (对于 DATE/DATETIME) 1970-01-01 到 2038-01-19 (对于 TIMESTAMP) ``` 提供的日期 `'12830-08-01'` 远远超出了上述范围,因此无法存储到列 `end_date` 中。 2. **输入验证不足** 如果未对用户输入或程序生成的数据进行严格的校验,则可能导致这种无效日期进入数据库操作流程。 --- ### 解决方案 #### 方法一:修正数据源中的非法日期值 检查并修改导致该问题的具体记录。例如,如果你发现某个字段被误设为未来的极端年份(如12830),可以将其调整为合理的数值(比如当前时间或其他合法值)。示例 SQL 修改命令如下: ```sql UPDATE your_table_name SET end_date = '9999-12-31' WHERE id = problematic_id; ``` #### 方法二:在应用层添加数据验证逻辑 确保所有传递给数据库的操作都经过合法性检查,避免类似超出范围的情况发生。可以在代码中加入条件判断: ```python import datetime def validate_date(date_str): try: # 尝试解析字符串为标准格式,并限制最大日期不超过指定范围 dt_obj = datetime.datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S') max_allowed_date = datetime.datetime(9999, 12, 31) # 最大允许日期设置 if dt_obj > max_allowed_date or dt_obj < datetime.datetime(1900, 1, 1): # 可选最小日期设置 raise ValueError("Date out of valid range") except Exception as e: print(f"Invalid date {date_str}: {e}") return False return True # 测试函数 test_dates = ["12830-08-01 00:00:00", "2024-05-20 12:30:00"] for d in test_dates: is_valid = validate_date(d) print(f"{d} -> {'Valid' if is_valid else 'Not Valid'}") ``` #### 方法三:更改表结构以适应更大范围的需求 如果业务场景确实需要处理非常遥远未来的时间点,考虑使用更大的容器替代默认的数据类型——将现有 DATETIME 改成 BIGINT 来直接保存 Unix 时间戳形式表示的大整数即可支持任意大小数字代表无限延伸时刻。 不过需注意这会牺牲掉一部分查询便利性和标准化规范优势! --- ### 总结建议 优先从源头解决问题,即保证正确的原始数据质量;其次增强系统健壮性的措施也很关键,包括但不限于前端界面提示、后端API接口约束以及SQL脚本层面的安全防护机制等多方面努力共同防范此类异常的发生概率降至最低限度之内。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值