尝试使用IViewObject :: Draw()将Web浏览器控件渲染到HDC中会失败,但IE11会成功

c00*_*0fd 4 c++ com internet-explorer mfc internet-explorer-8

我有一个MFC对话框窗口,我在其中添加了一个WebBrowser控件(封装了Internet Explorer引擎).

下面的代码的目标是到说的内容渲染web browser control成一个device context,我以后可以用于打印:

//MFC code, error checks are omitted for brevity

//'m_browser' = is a web browser control of type `CExplorer1`
IDispatch* pHtmlDoc = m_browser.get_Document();

CComPtr<IHTMLDocument2> pHtmlDocument2;
pHtmlDoc->QueryInterface(IID_IHTMLDocument2, (void**)&pHtmlDocument2));

//Get IViewObject2 for the entire document that we will use to render into a DC
CComPtr<IViewObject2> pViewObject;
pHtmlDocument2->QueryInterface(IID_IViewObject2, (void **)&pViewObject));

CComPtr<IHTMLElement> pBody;
pHtmlDocument2->get_body(&pBody));

CComPtr<IHTMLElement2> pBody2;
pBody->QueryInterface(IID_IHTMLElement2, (void **)&pBody2));


//Get default printer DC
CPrintDialog pd(TRUE, PD_ALLPAGES | PD_USEDEVMODECOPIES | PD_NOPAGENUMS | PD_HIDEPRINTTOFILE | PD_NOSELECTION);
pd.m_pd.Flags |= PD_RETURNDC | PD_RETURNDEFAULT;
pd.DoModal();         //corrected later
HDC hPrintDC = pd.CreatePrinterDC();

//Calc bitmap width based on printer DC specs
//Note that this width will be larger than
//the width of the WebControl window itself due to
//printer's much higher DPI setting...
int n_bitmapWidth = ::GetDeviceCaps(hPrintDC, HORZRES);     //Use entire printable area


//Get full size of the document
long n_scrollWidth;
long n_scrollHeight;
pBody2->get_scrollWidth(&n_scrollWidth);
pBody2->get_scrollHeight(&n_scrollHeight);

//Calc proportional size of the bitmap in the DC to render
int nWidth = n_bitmapWidth;
int nHeight = n_bitmapWidth * n_scrollHeight / n_scrollWidth;

//Create memory DC to render into
HDC hDc = ::GetDC(hWnd);
HDC hCompDc = ::CreateCompatibleDC(hDC);

//I'm using a raw DIB section here as I'll need to access
//its bitmap bits directly later in my code...
BITMAPINFOHEADER infoHeader = {0};

infoHeader.biSize          = sizeof(infoHeader);
infoHeader.biWidth         = nWidth;
infoHeader.biHeight        = -nHeight;
infoHeader.biPlanes        = 1;
infoHeader.biBitCount      = 24;
infoHeader.biCompression   = BI_RGB;

BITMAPINFO info;
info.bmiHeader = infoHeader; 

//Create a bitmap as DIB section of size `nWidth` by `nHeight` pixels
BYTE* pMemory = 0;
HBITMAP hBitmap = ::CreateDIBSection(hDc, &info, DIB_RGB_COLORS, (void**)&pMemory, 0, 0);

HBITMAP hOldBmp = (HBITMAP)::SelectObject(hCompDc, hBitmap);

RECT rcAll = {0, 0, nWidth, nHeight};
::FillRect(hCompDc, &rcAll, (HBRUSH)::GetStockObject(WHITE_BRUSH));

RECTL rectPrnt = {0, 0, nWidth, nHeight};

//Do the upscaling & render -- note that IE8 fails to render it here!!!!
pViewObject->Draw(DVASPECT_CONTENT, //DVASPECT_DOCPRINT
        -1, NULL, NULL, NULL, hCompDc, 
        &rectPrnt,
        NULL,
        NULL, 0));

::SelectObject(hCompDc, hOldBmp);


//Now the bitmap in `hCompDc` contains the resulting pixels
//For debugging purposes, save it as .bmp file

BITMAPFILEHEADER fileHeader = {0};
fileHeader.bfType      = 0x4d42;
fileHeader.bfSize      = 0;
fileHeader.bfReserved1 = 0;
fileHeader.bfReserved2 = 0;
fileHeader.bfOffBits   = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);

CFile file(
    L"path-to\\test.bmp",
    CFile::modeCreate | CFile::modeReadWrite | CFile::shareDenyNone);
file.Write((char*)&fileHeader, sizeof(fileHeader));
file.Write((char*)&infoHeader, sizeof(infoHeader));

int bytes = (((24 * nWidth + 31) & (~31)) / 8) * nHeight;
file.Write(pMemory, bytes);


//Clean up
::DeleteObject(hBitmap);
::DeleteDC(hCompDc);

::ReleaseDC(hWnd, hDc);

::DeleteDC(hPrintDC);
Run Code Online (Sandbox Code Playgroud)

如果我IE11在开发机器上安装了最新版本,此代码可以正常工作.但是,例如,如果有人IE8在他们的Windows 7 上安装,则IViewObject :: Draw方法将仅呈现文档的一小部分(等于Web浏览器控件本身的大小.)

描述它的最好方法是用例子来说明它:

通常呈现的测试页面IE11已安装:

在此输入图像描述

以下是IE8安装后发生的情况:

在此输入图像描述

有谁不知道我在这里做错了什么IE8不喜欢?

编辑1:进一步深入了解IViewObject::Draw函数,WinDbg然后找到它的源代码.这是CServer :: Draw(),IViewObject::Draw然后是内部调用的CDoc :: Draw()CServer::Draw().

Fed*_*sev 5

首先,感谢有趣的问题.虽然不那么实用 - 今天很多人都没有使用IE8 - 解决这个问题并不是那么简单.我将描述问题是什么,并提供一个简单但可行的解决方案,您可以改进.


在我进入IE8解决方案之前,有几点:

  1. 如果您遇到大型文档,则窗口调整大小以适合滚动大小的解决方案不稳定.如果您不知道预期的内容,则还需要为以后的资源管理器重构解决方案,以避免依赖于调整窗口大小来滚动大小.

  2. 为什么要携带巨型位图?图元文件等可能更合适.在这个分辨率下任何足够大的页面都将在PC上用大量的DIB创建来释放内存.提供的示例中的Google页面呈现为100Mb位图文件,而执行光栅化的emf则需要不到1Mb.

  3. 虽然我不知道您的项目的确切要求和限制,但我99%确信吸入巨大的DIB并不是最好的解决方案.甚至EMF虽然更好,但也不是最好的.例如,如果您需要添加签名然后打印,则有更好的方法来处理此问题.当然,这只是一个旁注,与问题本身无关.


IE8渲染问题

在IE8渲染器中有一个错误.Draw()将被剪切在实际显示区域的像素尺寸上(您看到的可见矩形是渲染上下文比例中的原始显示区域).

因此,如果您的缩放目标大于实际大小,则在缩放时,它将被缩放到缩放像素的大小(因此它的内容比原始矩形少得多).

如果它没有为真正的IE8上的某人剪辑,那么系统中存在后续IE的剩余部分,或者存在其他非临时设置,系统更新等.

解决方法可能性

好消息是可以解决方法,坏消息的解决方法有点令人讨厌.

首先,仍然可以使用IViewObject进行解决.但是,因为涉及任意缩放并且可访问的源矩形非常小,所以这种解决方案具有一些复杂性,我认为这些都是SO答案.所以我不会潜入这条道路.

相反,我们可以渲染另一个现在过时的API:IHTMLElementRender.它允许使用DrawToDC将页面呈现为任意上下文.不幸的是,它并不像看起来那么简单,不仅仅是提供设备上下文.

首先,有类似的剪辑错误.它可以更容易处理,因为剪切发生在超出屏幕尺寸的大值.其次,当使用设备上下文转换时,它将无法工作或将混乱渲染的html,因此您实际上不能依赖于缩放或转换.这两个问题都需要相对不重要的处理并且彼此复杂化.

解决方案

我将描述并提供非最佳的示例代码,但是在大多数简单页面解决方案上工作.通常,可以实现完美且更有效的解决方案,但是,这又超出了答案的范围.显然,它只是IE8,所以你需要检查浏览器版本并为IE8和IE9或更高版本执行不同的处理程序,但你也可以采取一些想法来改进其他浏览器的渲染.

这里有两个相互关联的解决方法.

向上扩展

首先,如果我们无法转换,我们如何将矢量内容升级到打印机质量?此处的解决方法是渲染到与打印机DC兼容的上下文.会发生什么是内容将在打印机DPI上呈现.请注意,它不能完全适合打印机宽度,它将扩展到printerDPI/screenDPI.

之后,在光栅化时,我们缩小尺寸以适应打印机宽度.我们最初渲染到EMF,因此没有太多质量损失(无论如何都会在打印机本身上发生).如果你需要更高的质量(我怀疑它),有两种可能性 - 修改解决方案以渲染目标宽度(这不是微不足道)或使用生成的emf而不是位图,让打印机使缩小比例适合.

另一个注意事项是,您目前只使用打印机宽度,但您可能需要从打印机查询并为帐户进行查询,这些页边距可能不可打印.因此,即使您提供精确打印机尺寸的位图,它也可能会被打印机重新缩放.但同样,我怀疑这种解决方案的差异会对你的项目产生任何影响.

剪裁

要克服的第二个是削减.为了克服这个限制,我们以小块的形式呈现内容,这样它们就不会被渲染器剪切掉.渲染块后,我们更改文档的滚动位置,并将下一个块渲染到目标DC中的适当位置.这可以优化为使用更大的块,例如最近的DPI倍数到1024(使用窗口调整大小),但我没有实现它(它只是速度优化).如果不进行此优化,请确保最小浏览器窗口大小不会太小.

注意,在任意小数刻度上执行此滚动将是近似值,并且在通用情况下实现起来并不那么简单.但是使用常规打印机和屏幕,我们可以在DPI的倍数中进行整数块步骤,例如,如果屏幕是96 DPI而打印机是600DPI,我们在每个上下文中以96和600的相同倍数执行步骤,一切都更简单.但是,处理完所有整块后的顶部或底部的余数不会在DPI中增加,所以我们不能轻易地滚动.

通常,我们可以在打印机空间中近似滚动位置,并希望最终块之间不会出现错误配置.我所做的是在页面的右下方添加一个绝对定位的div,块大小.

请注意,这可能会干扰某些页面并更改布局(可能不是简单报告的情况).如果这是一个问题,您需要在循环后添加余数处理而不是添加元素.在这种情况下,最简单的解决方案仍然是使用div填充,但不是使用完整的块大小,而只是使内容宽度为屏幕DPI的倍数.

更简单的想法,正如我后面所意识到的,只是将窗口调整到最接近的DPI倍数,并将此窗口大小作为块大小.您可以尝试使用而不是div,这将简化代码并修复可能会干扰注入的div的页面.

代码

这只是一个样本.

  • 没有错误处理.您需要为每个COM和API调用添加检查等.

  • 没有代码风格,只是快速和肮脏.

  • 不确定所有获得的资源是否按需发布,请进行检查

  • 必须在浏览器控件上禁用页面边框才能使此示例正常工作(如果您需要浏览器边框,只需单独渲染它们,内置程序就不一致了).在IE8上,这不是那么微不足道,但在这里或网上有很多答案.无论如何,我将在示例解决方案项目中包含此补丁.您也可以使用边框进行渲染并将其排除,但对于具有简单解决方案的问题,这将是不必要的复杂化.

  • 完整的解决方案项目可以在这个链接找到,我只会在这里发布相关的代码.

下面的代码呈现页面并保存在c:\ temp\test.emf + c:\ temp\test.bmp中

void convertEmfToBitmap(const RECT& fitRect, HDC hTargetDC, HENHMETAFILE hMetafile, LPCTSTR fileName);
CComPtr<IHTMLDOMNode> appendPadElement(IHTMLDocument2* pDoc, IHTMLElement* pBody, long left, long top, long width, long height);
void removeElement(IHTMLElement* pParent, IHTMLDOMNode* pChild);

void CMFCApplication1Dlg::OnBnClickedButton2()
{
    COleVariant varNull;
    COleVariant varUrl = L"http://www.google.com/search?q=ie+8+must+die";
    m_browser.Navigate2(varUrl, varNull, varNull, varNull, varNull);
}


void CMFCApplication1Dlg::OnBnClickedButton1()
{
    //get html interfaces
    IDispatch* pHtmlDoc = m_browser.get_Document();
    CComPtr<IHTMLDocument2> pHtmlDocument2;
    pHtmlDoc->QueryInterface(IID_IHTMLDocument2, (void**)&pHtmlDocument2);

    CComPtr<IHTMLElement> pBody;
    pHtmlDocument2->get_body(&pBody);

    CComPtr<IHTMLElement2> pBody2;
    pBody->QueryInterface(IID_IHTMLElement2, (void**)&pBody2);

    CComPtr<IHTMLBodyElement> pBodyElement;
    pBody->QueryInterface(IID_IHTMLBodyElement, (void**)&pBodyElement);

    CComPtr<IHTMLElement> pHtml;
    pBody->get_parentElement(&pHtml);

    CComPtr<IHTMLElement2> pHtml2;
    pHtml->QueryInterface(IID_IHTMLElement2, (void**)&pHtml2);

    CComPtr<IHTMLStyle> pHtmlStyle;
    pHtml->get_style(&pHtmlStyle);
    CComPtr<IHTMLStyle> pBodyStyle;
    pBody->get_style(&pBodyStyle);

    //get screen info
    HDC hWndDc = ::GetDC(m_hWnd);
    const int wndLogPx = GetDeviceCaps(hWndDc, LOGPIXELSX);
    const int wndLogPy = GetDeviceCaps(hWndDc, LOGPIXELSY);


    //keep current values
    SIZE keptBrowserSize = { m_browser.get_Width(), m_browser.get_Height() };
    SIZE keptScrollPos;
    //set reasonable viewport size 
    //m_browser.put_Width(docSize.cx);
    //m_browser.put_Height(docSize.cy*2);
    pHtml2->get_scrollLeft(&keptScrollPos.cx);
    pHtml2->get_scrollTop(&keptScrollPos.cy);
    COleVariant keptOverflow;
    pBodyStyle->get_overflow(&keptOverflow.bstrVal);

    //setup style and hide scroll bars
    pHtmlStyle->put_border(L"0px;");
    pHtmlStyle->put_overflow(L"hidden");
    pBodyStyle->put_border(L"0px;");
    pBodyStyle->put_overflow(L"hidden");

    //get document size and visible area in screen pixels
    SIZE docSize;
    pBody2->get_scrollWidth(&docSize.cx);
    pBody2->get_scrollHeight(&docSize.cy);
    RECT clientRect = { 0 };
    pHtml2->get_clientWidth(&clientRect.right);
    pHtml2->get_clientHeight(&clientRect.bottom);

    //derive chunk size
    const SIZE clientChunkSize = { 
        clientRect.right - clientRect.right % wndLogPx, 
        clientRect.bottom - clientRect.bottom % wndLogPy };

    //pad with absolutely positioned element to have enough scroll area for all chunks
    //alternatively, browser can be resized to chunk multiplies (simplest), to DPI multiplies (more work). 
    //This pad also can be made smaller, to modulus DPI, but then need more work in the loops below
    CComPtr<IHTMLDOMNode> pPadNode = 
        appendPadElement(pHtmlDocument2, pBody, docSize.cx, docSize.cy, clientChunkSize.cx, clientChunkSize.cy);

    //get printer info
    CPrintDialog pd(TRUE, PD_ALLPAGES | PD_USEDEVMODECOPIES | PD_NOPAGENUMS | PD_HIDEPRINTTOFILE | PD_NOSELECTION);
    pd.m_pd.Flags |= PD_RETURNDC | PD_RETURNDEFAULT;
    pd.DoModal(); 
    HDC hPrintDC = pd.CreatePrinterDC();
    const int printLogPx = GetDeviceCaps(hPrintDC, LOGPIXELSX);
    const int printLogPy = GetDeviceCaps(hPrintDC, LOGPIXELSY);
    const int printHorRes = ::GetDeviceCaps(hPrintDC, HORZRES);
    const SIZE printChunkSize = { printLogPx * clientChunkSize.cx / wndLogPx, printLogPy * clientChunkSize.cy / wndLogPy };

    //browser total unscaled print area in printer pixel space
    const RECT printRectPx = { 0, 0, docSize.cx* printLogPx / wndLogPx, docSize.cy*printLogPy / wndLogPy };
    //unscaled target EMF size in 0.01 mm with printer resolution
    const RECT outRect001Mm = { 0, 0, 2540 * docSize.cx / wndLogPx, 2540 * docSize.cy / wndLogPy };
    HDC hMetaDC = CreateEnhMetaFile(hPrintDC, L"c:\\temp\\test.emf", &outRect001Mm, NULL);
    ::FillRect(hMetaDC, &printRectPx, (HBRUSH)::GetStockObject(BLACK_BRUSH));

    //unscaled chunk EMF size in pixels with printer resolution
    const RECT chunkRectPx = { 0, 0, printChunkSize.cx, printChunkSize.cy };
    //unscaled chunk EMF size in 0.01 mm with printer resolution
    const RECT chunkRect001Mm = { 0, 0, 2540 * clientChunkSize.cx / wndLogPx, 2540 * clientChunkSize.cy / wndLogPy };

    ////////
    //render page content to metafile by small chunks

    //get renderer interface
    CComPtr<IHTMLElementRender> pRender;
    pHtml->QueryInterface(IID_IHTMLElementRender, (void**)&pRender);
    COleVariant printName = L"EMF";
    pRender->SetDocumentPrinter(printName.bstrVal, hMetaDC);


    //current positions and target area
    RECT chunkDestRectPx = { 0, 0, printChunkSize.cx, printChunkSize.cy };
    POINT clientPos = { 0, 0 };
    POINT printPos = { 0, 0 };

    //loop over chunks left to right top to bottom until scroll area is completely covered
    const SIZE lastScroll = { docSize.cx, docSize.cy};
    while (clientPos.y < lastScroll.cy)
    {
        while (clientPos.x < lastScroll.cx)
        {
            //update horizontal scroll position and set target area
            pHtml2->put_scrollLeft(clientPos.x);
            chunkDestRectPx.left = printPos.x;
            chunkDestRectPx.right = printPos.x + printChunkSize.cx;

            //render to new emf, can be optimized to avoid recreation
            HDC hChunkDC = CreateEnhMetaFile(hPrintDC, NULL, &chunkRect001Mm, NULL);
            ::FillRect(hChunkDC, &chunkRectPx, (HBRUSH)::GetStockObject(WHITE_BRUSH));
            pRender->DrawToDC(hChunkDC);
            HENHMETAFILE hChunkMetafile = CloseEnhMetaFile(hChunkDC);

            //copy chunk to the main metafile
            PlayEnhMetaFile(hMetaDC, hChunkMetafile, &chunkDestRectPx);
            DeleteEnhMetaFile(hChunkMetafile);

            //update horizontal positions
            clientPos.x += clientChunkSize.cx;
            printPos.x += printChunkSize.cx;
        }

        //reset horizontal positions
        clientPos.x = 0;
        printPos.x = 0;
        //update vertical positions
        clientPos.y += clientChunkSize.cy;
        printPos.y += printChunkSize.cy;
        pHtml2->put_scrollTop(clientPos.y);
        chunkDestRectPx.top = printPos.y;
        chunkDestRectPx.bottom = printPos.y + printChunkSize.cy;
    }

    //restore changed values on browser
    //if for large pages on slow PC you get content scrolling during rendering and it is a problem,
    //you can either hide the browser and show "working" or place on top first chunk content
    pBodyStyle->put_overflow(keptOverflow.bstrVal);
    pHtml2->put_scrollLeft(keptScrollPos.cx);
    pHtml2->put_scrollTop(keptScrollPos.cy);
    m_browser.put_Width(keptBrowserSize.cx);
    m_browser.put_Height(keptBrowserSize.cy);
    removeElement(pBody, pPadNode);

    //draw to bitmap and close metafile
    HENHMETAFILE hMetafile = CloseEnhMetaFile(hMetaDC);
    RECT fitRect = { 0, 0, printHorRes, docSize.cy * printHorRes / docSize.cx };
    convertEmfToBitmap(fitRect, hWndDc, hMetafile, L"c:\\temp\\test.bmp");
    DeleteEnhMetaFile(hMetafile);

    //cleanup - probably more here
    ::ReleaseDC(m_hWnd, hWndDc);
    ::DeleteDC(hPrintDC);

    //{
    //  std::stringstream ss;
    //  ss << "====" << docSize.cx << "x" << docSize.cy << " -> " << fitRect.right << "x" << fitRect.bottom << "" << "\n";
    //  OutputDebugStringA(ss.str().c_str());
    //}

}



///////////////
////some util 

void convertEmfToBitmap(const RECT& fitRect, HDC hTargetDC, HENHMETAFILE hMetafile, LPCTSTR fileName)
{
    //Create memory DC to render into
    HDC hCompDc = ::CreateCompatibleDC(hTargetDC);
    //NOTE this 
    BITMAPINFOHEADER infoHeader = { 0 };
    infoHeader.biSize = sizeof(infoHeader);
    infoHeader.biWidth = fitRect.right;
    infoHeader.biHeight = -fitRect.bottom;
    infoHeader.biPlanes = 1;
    infoHeader.biBitCount = 24;
    infoHeader.biCompression = BI_RGB;

    BITMAPINFO info;
    info.bmiHeader = infoHeader;

    //create bitmap
    BYTE* pMemory = 0;
    HBITMAP hBitmap = ::CreateDIBSection(hCompDc, &info, DIB_RGB_COLORS, (void**)&pMemory, 0, 0);
    HBITMAP hOldBmp = (HBITMAP)::SelectObject(hCompDc, hBitmap);


    PlayEnhMetaFile(hCompDc, hMetafile, &fitRect);

    BITMAPFILEHEADER fileHeader = { 0 };
    fileHeader.bfType = 0x4d42;
    fileHeader.bfSize = 0;
    fileHeader.bfReserved1 = 0;
    fileHeader.bfReserved2 = 0;
    fileHeader.bfOffBits = sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER);

    CFile file(
        fileName,
        CFile::modeCreate | CFile::modeReadWrite | CFile::shareDenyNone);
    file.Write((char*)&fileHeader, sizeof(fileHeader));
    file.Write((char*)&infoHeader, sizeof(infoHeader));

    int bytes = (((24 * infoHeader.biWidth + 31) & (~31)) / 8) * abs(infoHeader.biHeight);
    file.Write(pMemory, bytes);

    ::SelectObject(hCompDc, hOldBmp);

    //Clean up
    if (hBitmap)
        ::DeleteObject(hBitmap);
    ::DeleteDC(hCompDc);
}


CComPtr<IHTMLDOMNode> appendPadElement(IHTMLDocument2* pDoc, IHTMLElement* pBody, long left, long top, long width, long height)
{
    CComPtr<IHTMLElement> pPadElement;
    pDoc->createElement(L"DIV", &pPadElement);
    CComPtr<IHTMLStyle> pPadStyle;
    pPadElement->get_style(&pPadStyle);
    CComPtr<IHTMLStyle2> pPadStyle2;
    pPadStyle->QueryInterface(IID_IHTMLStyle2, (void**)&pPadStyle2);
    pPadStyle2->put_position(L"absolute");
    CComVariant value = width;
    pPadStyle->put_width(value);
    value = height;
    pPadStyle->put_height(value);
    pPadStyle->put_posLeft((float)left);
    pPadStyle->put_posTop((float)top);
    CComPtr<IHTMLDOMNode> pPadNode;
    pPadElement->QueryInterface(IID_IHTMLDOMNode, (void**)&pPadNode);
    CComPtr<IHTMLDOMNode> pBodyNode;
    pBody->QueryInterface(IID_IHTMLDOMNode, (void **)&pBodyNode);
    pBodyNode->appendChild(pPadNode, NULL);
    return pPadNode;
}

void removeElement(IHTMLElement* pParent, IHTMLDOMNode* pChild)
{
    CComPtr<IHTMLDOMNode> pNode;
    pParent->QueryInterface(IID_IHTMLDOMNode, (void **)&pNode);
    pNode->removeChild(pChild, NULL);
}
Run Code Online (Sandbox Code Playgroud)

样本页输出(4958x7656)

样本输出