65.9K
CodeProject 正在变化。 阅读更多。
Home

如何打印 CFormView

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (17投票s)

2003年8月19日

5分钟阅读

viewsIcon

95830

downloadIcon

3199

本文提供了两种不同的打印 CFormView 的方法。

引言

您是否曾想过如何在 MFC 中打印 CFormView?关于这个主题有一些文章,但它们似乎存在一些普遍的错误。在本文中,提供了两种不同的方法来实现这一点。在文章的最后部分,我们将指出每种方法的优点和缺点。

MFC 打印工作原理

在我们继续之前,我们需要了解 MFC 中的 OnPrintPreviewOnPrint 是如何工作的。下面是代码的草稿。

  1. 调用 OnPreparePrint 来弹出对话框以设置实际打印机和页数。通常,我们会让 MFC 在此完成工作。但是,如果您想跳过对话框并直接设置打印机 DC,最好在此处执行此操作,或者完全重写 OnPrint/OnPrintPreview
  2. 调用 OnBeginPrint。如果我们能确定需要打印的页数,最好在此处设置页数。
  3. 循环打印每一页。
    • 在循环中,调用 OnPrepareDC。如果您需要设置映射模式以及逻辑像素和物理像素之间的转换比率,例如屏幕上的一个英寸等于打印机上的一个英寸,最好在此处执行此操作。要牢记的一个重要事项是,如果您在步骤 2 中无法确定需要打印的页数,您仍然可以使用 CPrintInfo::m_bContinuePrinting 成员变量来终止打印。
    • 调用 OnPrint 来执行实际打印。

好了,就是这样。是时候卷起袖子做些脏活了。我们拿的就是这个报酬,对吧?

捕获 CFormView 的屏幕图像

伙计,这家伙在这里做什么,浪费我的时间,哈?你可能这样想,因为已经有一些关于这个主题的文章了。异议听到,但他们做对了吗?

与将任何代码注入框架一样,首先需要知道将代码添加到哪里。在 MFC 中编程时,这类问题始终是最棘手的。在这种情况下,问题是何时抓取图像。在 OnBeginPrint 中进行怎么样?乍一看并非坏主意。好吧,事实证明这里有个陷阱。由于 MFC 会弹出一个窗口来模拟预览模式下的打印机 DC,因此您最终可能会在此模式下捕获错误的图像。最好在 OnFilePrintOnFilePrintPreview 中执行此操作。实际代码如下所示:

void CFormViewPrintView::_grapImage( ) 
{
    //Grap Image
    CPoint oldPoint = GetScrollPosition( );
    //scroll to top left corner as CFormView is a Scroll view
    CPoint pt( 0, 0 );
    this->ScrollToPosition( pt );

    CClientDC dc(this);
    CRect rect;
    this->GetClientRect( rect );
    m_dib.Attach( GDIUtil::GrabDIB( &dc, rect ) );

    ScrollToPosition( oldPoint );
}

void CFormViewPrintView::OnFilePrintPreview() 
{
    // TODO: Add your command handler code here
    _grapImage( );
    CFormView::OnFilePrintPreview() ;
}

void CFormViewPrintView::OnFilePrint() 
{
    _grapImage( );
    CFormView::OnFilePrint() ;
}

嗯,GDIUtil::GradDIB 是做什么的?它从屏幕抓取位图并将其转换为 DIB。为什么要 DIB,而不是直接使用位图?位图始终依赖于 DC,而屏幕 DC 与打印机 DC 不同。没有这种转换,我们就受打印机驱动程序的摆布。在某些打印机上可能效果很好,但在其他打印机上可能效果很差。请参阅 Roger Allen 的文章

接下来,我们需要处理如何保持显示在屏幕上的某些内容具有相同的大小。是否曾想过为什么打印时某些东西会变得异常小?原因如下:假设打印机的分辨率为每英寸 600 像素,而屏幕上的分辨率通常为每英寸 96 或 120 像素。如果您只是以像素为单位打印“相同大小”的内容,那么很容易想象会发生什么。这也是为什么打印文本时应该更改字体大小的原因。我们真正想要的是以英寸为单位打印相同大小的内容,而不是以像素为单位。“明白了,但是要把这种转换的代码放在哪里?”您问自己,并意识到这又是那个老生常谈的“放在哪里”的问题。这可以通过重写 OnPrepareDC 方法来完成。微软的这个名称实际上是指“在此处设置映射模式(如果需要)”。这也是决定是终止打印还是不终止打印的地方,如果您以前还没有确定打印页数。我们的 OnPrepareDC 如下所示:

void CFormViewPrintView::OnPrepareDC(CDC* pDC, 
                      CPrintInfo* pInfo /* = NULL */)
{
    // TODO: Add your specialized code here and/or call the base class
    if( pInfo )
    {
        CClientDC dc( this );
        pDC->SetMapMode(MM_ANISOTROPIC);

        CSize sz( dc.GetDeviceCaps(LOGPIXELSX), 
                    dc.GetDeviceCaps(LOGPIXELSY) );
        pDC->SetWindowExt( sz );
        sz = CSize( pDC->GetDeviceCaps(LOGPIXELSX),
                        pDC->GetDeviceCaps(LOGPIXELSY) );
        pDC->SetViewportExt( sz );
    }
}

这段代码是什么意思?它意味着屏幕上的一个英寸,在这种情况下是 dc,等于打印机上的一个英寸(可能是伪一个英寸),我们不关心实际像素大小的变化,例如屏幕上的 120 ppi 与打印机上的 600 ppi。

最后,实际打印。

void CFormViewPrintView::OnPrint(CDC* pDC, CPrintInfo* pInfo)
{
    // TODO: add customized printing code here
    if( pInfo == NULL )
        return;

    if( m_dib.GetHandle( ) == NULL )
        return;
    {
        //Call GlobalLock in constructor, call Unlock when exists the block
        GLock lock( m_dib );
        BITMAPINFOHEADER *pBMI = (BITMAPINFOHEADER*)(LPVOID)lock;

        int nColors = 0;
        if( pBMI->biBitCount <= 8 )
            nColors = ( 1<< pBMI->biBitCount );

        ::StretchDIBits( pDC->GetSafeHdc( ),
        pInfo->m_rectDraw.left, 
        pInfo->m_rectDraw.top,
        pBMI->biWidth,
        pBMI->biHeight,
                0, 
                0, 
                pBMI->biWidth,
                pBMI->biHeight,
                (LPBYTE)pBMI + (pBMI->biSize + nColors * sizeof(RGBQUAD)),
                (BITMAPINFO*)pBMI,
                DIB_RGB_COLORS, 
                SRCCOPY);
    }
}

需要提到的一点是,GUtil 中的 GLock 的理念与 STD 中的 AutoPtr 相同。我不知道为什么微软在 CClientDCCPaintDC 中做得很好,但在处理 GlobalLock/Unlock 或臭名昭著的 SelectObject 时却视而不见。我们有多少次抓耳挠腮地查找 GDI 对象资源泄露,结果却发现我们选择了一个对象,却忘记了把它选出来。

另一种方法 WM_PRINT 消息

您是否听说过 WM_PRINT 消息?它甚至不在 Visual C++ 类向导中,但它似乎有望实现我们打印 CFormView 所需的一切。这里是打印 CFormView 的另一种方法:

void CFormViewPrint2View::_print( )
{
    CRect rect;
    this->GetClientRect( rect );
    CDC memDC;

    CClientDC dc( this );
    memDC.CreateCompatibleDC( &dc );

    CBitmap bitmap;
    bitmap.CreateCompatibleBitmap( &dc, rect.Width(), rect.Height() );
    {
        //This will force bitmap selected out of DC when exit this block
        LocalGDI local( &memDC, &bitmap );
        this->Print( &memDC, PRF_ERASEBKGND|PRF_CLIENT|PRF_CHILDREN );
    }
    m_dib.Attach( GDIUtil::DDBToDIB( bitmap ) );
}

void CFormViewPrint2View::OnFilePrintPreview() 
{
    // TODO: Add your command handler code here
    _print( );
    CFormView::OnFilePrintPreview( );
}

void CFormViewPrint2View::OnFilePrint() 
{
    // TODO: Add your command handler code here
    _print( );
    CFormView::OnFilePrint( );
}

结论

那么,每种方法的优缺点是什么?第一种方法不关心您有多少个单独的子控件以及如何打印它们到打印机,但它只能打印屏幕的视觉部分。而第二种方法看起来比第一种方法好得多、干净得多,它甚至允许您打印整个客户区而不将其显示在屏幕上。不幸的是,它也有一个陷阱。一些子类化的 Windows 控件和用户自定义控件可能根本不处理 WM_PRINT 消息,如果您可以处理 WM_PAINT 消息,实现起来会非常简单。

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.