5.4 使用打印框架

我们已经介绍过,可以直接使用wxPrinterDC来进行打印。不过,一个更灵活的方法是使用wxWidgets提供的打印框架来驱动打印机。要使用这个框架,最主要的任务就是要实现一个wxPrintout的派生类,重载其成员函数以便告诉wxWidgets怎样打印一页(OnPrintPage), 总共有多少页(GetPageInfo),进行页面设置(OnPreparePrinting)等等。而wxWidgets框架则负责显示打印对话框,创建打印设备上下文和调用适当的wxPrintout的函数。同一个wxPrintout类将被打印和预览功能一起使用。

当要开始打印的时候,一个wxPrintout对象实例被传递给wxPrinter对象,然后将调用Print函数开始打印过程,并且在准备打印用户指定的那些页面前显示一个打印对话框。如下面例子中的那样。

// 一个全局变量用来存储打印相关的设置信息
wxPrintDialogData g_printDialogData;
// 用户从主菜单中选择打印命令以后
void MyFrame::OnPrint(wxCommandEvent& event)
{
    wxPrinter printer(& g_printDialogData);
    MyPrintout printout(wxT("My printout"));
    if (!printer.Print(this, &printout, true))
    {
        if (wxPrinter::GetLastError() == wxPRINTER_ERROR)
            wxMessageBox(wxT("There was a problem printing.\nPerhaps your current printer
 is not set correctly?"), wxT("Printing"), wxOK);
        else
            wxMessageBox(wxT("You cancelled printing"),
                         wxT("Printing"), wxOK);
    }
    else
    {
        (*g_printDialogData) = printer.GetPrintDialogData();
    }
}

因为打印函数在所有的页面都已经被渲染和发送到打印机以后才会返回,因此wxPrintout对象可以以局部变量的方式创建。

wxPrintDialogData对象用来存储所有打印有关的数据,比如用户选择的页面和份数等。将其保存在一个全局变量中并且在下次调用的时候传递给wxPrinter对象是一个不错的习惯,因此象上面代码中演示的那样,在创建wxPrinter的时候传递给它一份全局配置数据的指针,并在Print函数成功返回以后将当前的打印数据保存在全局变量里(当然,一个更专业的作法是将其保存在你的应用程序类中)。关于使用打印和页面设置对话框的详情请参考第8章,�使用标准对话框�。

如果要创建打印预览,你需要创建一个wxPrintPreview对象,给这个对象传递两个wxPrintout对象作为参数,一个用于预览,一个则用于在用户预览的时候直接请求打印,同样的,你可以传递一个全局的wxPrintDialogData对象给预览对象以便预览类使用用户以前选择的打印设置数据。然后将这个预览类传递给wxPreviewFrame,然后调用wxPreviewFrame的Initialize函数和 Show函数显示这个窗口,如下所示:

// 用户选择打印预览菜单命令
void MyFrame::OnPreview(wxCommandEvent& event)
{
    wxPrintPreview *preview = new wxPrintPreview(
                             new MyPrintout, new MyPrintout,
                             & g_printDialogData);
    if (!preview->Ok())
    {
        delete preview;
        wxMessageBox(wxT("There was a problem previewing.\nPerhaps your current printer is
 not set correctly?"),
                     wxT("Previewing"), wxOK);
        return;
    }
    wxPreviewFrame *frame = new wxPreviewFrame(preview, this,
                             wxT("Demo Print Preview"));
    frame->Centre(wxBOTH);
    frame->Initialize();
    frame->Show(true);
}

当打印预览窗口被初始化的时候,它将禁用所有其它的顶层窗口以确保任何可能导致正在预览或者打印的文档内容发生改变的可能。关闭这个窗口将会导致两个wxPrintout对象自动被释放。下图显示了预览窗口的样子,可以看到它内含一个工具条,上面提供了页面遍历,打印以及缩放控制等功能。

关于wxPrintout的更多内容

当创建wxPrintout对象的时候,你可以传递一个可选的标题参数,在某些操作系统上,这个标题将显示在打印管理器上。另外,要重载一个 wxPrintout对象,你至少需要重载GetPageInfo, HasPage和OnPrintPage函数,当然,下面介绍的这些函数你都可以视需要进行重载。

首先来介绍GetPageInfo函数,它用来返回最小页码,最大页码,开始打印页码和结束打印页码。前两个参数用来提供一个可以打印的范围,后两个参数被设计来反应用户选择的页码范围,不过目前没有用处。最小页面的默认值为1,最大页码的默认值为32000,不过当HasPage函数返回False的时候,wxPrintout类也将停止打印。通常情况下,你的OnPreparePrinting应该计算当前打印内容在当前设置下的打印页数,将其保存在一个成员变量中,以便GetPageInfo函数返回正确的值,如下所示:

void MyPrintout::GetPageInfo(int *minPage, int *maxPage,
                             int *pageFrom, int *pageTo)
{
    *minPage = 1; *maxPage = m_numPages;
    *pageFrom = 1; *pageTo = m_numPages;
}

而HasPage函数则用来返回是否拥有某个页码,如果这个页码超出了最大页码的范围则必须返回False。通常你的实现类似下面的样子:

bool MyPrintout::HasPage(int pageNum)
{
    return (pageNum >= 1 && pageNum <= m_numPages);
}

OnPreparePrinting在预览或者打印过程刚开始的时候被调用,重载它使得应用程序可以进行各种设置工作,比如计算文档的总页数等。OnPreparePrinting可以调用wxPrintout的GetDC,GetPageSizeMM,IsPreview等函数,因此方便获取这些信息。

OnBeginDocument是在每次每篇文档即将开始打印的时候被调用的,如果这个函数被重载了,那么必须在重载的函数中调用 wxPrintout::OnBeginDocument。同样的wxPrintout::OnEndDocument也必须被它的重载函数调用。

OnBeginPrinting和OnEndPrinting函数则是在整个打印过程开始和结束的时候被调用,和当前打印多少份没有关系。

OnPrintPage会被传递一个页码参数,应用程序必须重载这个函数并且在成功打印这一页以后返回true。这个函数应该使用wxPrintout::GetDC来取得打印设备上下文已经进行绘画(打印)工作。

下面这些函数作为一些工具函数,你可以在你的重载函数中使用它们,而无需重载它们。

IsPreview函数用来检测当前正处于一个预览过程中还是一个真的打印过程中。

GetDC函数为当前正在进行的工作返回一个合适的设备上下文。如果是在真实的打印过程中,则返回一个wxPrinterDC,如果是预览,则返回一个wxMemoryDC,因为预览其实是在一个位图上通过内存设备上下文来渲染的。

GetPageSizeMM以毫米为单位返回当前打印页面的大小。GetPageSizePixels则以象素为单位返回这个值(打印机的最大分辨率)。如果是在预览过程中,这个大小和wxDC::GetSize返回的值通常是不一样大的(参见下面的说明),wxDC::GetSize返回用于预览的位图的大小。

GetPPIPrinter返回当前设备上下文每一个英寸对应的象素的数目,而GetPPIScreen则返回当前屏幕上每英寸对应的象素的数目。

打印和预览过程中的缩放

当你在窗口上绘画的时候,你可能不大关心图片的缩放,因为显示器的分辨率大部分都是相同的。然后,在面对一个打印机的时候,有几方面的因素可能导致你必须关心这个问题:

你需要通过缩放和重新放置图片的位置来保证图片位于某个页面之内,在某种情况下,你甚至可能需要把一个图片分成两半。

字体都是基于屏幕分辨率的,因此在打印文本的时候,你需要设置一个合适的缩放的值,以便使打印设备上下文符合屏幕的分辨率。在打印文本的时候,通过应该设置通过用GetPPIPrinter的值除以GetPPIScreen的值计算而得的值作为缩放因子的值。

当渲染预览图案的时候,wxWidgets使用了wxMemoryDC在一个位图上绘画。这个位图的大小(wxDC::GetSize)是基于当前预览的放大倍数的这就需要一个额外的缩放因子。通常这个缩放因子可以通过用GetSize返回的值除以GetPageSizePixels返回的实际页面的象素值来获得。通常这个值还应该乘以别的缩放因子定义的值。

你可以调用wxDC::SetUserScale来设置设备上下文的缩放因子,使用wxDC::SetDeviceOrigin来设置平移因子(例如,需要把一个图片放置在页面正中的时候)。如果有必要的话,你甚至可以在同一个页面绘画的时候反复使用不同的值来调用这两个函数。

wxWidgets的例子中的samples/printing演示了怎样在打印过程中使用缩放,下面列出的代码是其中进行缩放的适配代码,它将演示了在打印过程中和预览过程中将一幅大小为200x200象素的图片进行了缩放和放置,如下所示:

void MyPrintout::DrawPageOne(wxDC *dc)
{
    // 下面的代码可以这样写只是因为我们知道图片的大小是200x200
    // 如果我们不知道的话,需要先计算图片的大小
    float maxX = 200;
    float maxY = 200;
    // 让我们先设置至少50个设备单位的边框
    float marginX = 50;
    float marginY = 50;
    // 将边框的大小增加到图片的周围
    maxX += (2*marginX);
    maxY += (2*marginY);
    // 获取象素单位的当前设备上下文的大小
    int w, h;
    dc->GetSize(&w, &h);
    //计算一个合适的缩放值
    float scaleX=(float)(w/maxX);
    float scaleY=(float)(h/maxY);
    // 选择X或者Y方向上较小的那个
    float actualScale = wxMin(scaleX,scaleY);
    // 计算图片在设备上的合适位置以便居中
    float posX = (float)((w - (200*actualScale))/2.0);
    float posY = (float)((h - (200*actualScale))/2.0);
    // 设置设备平移和缩放
    dc->SetUserScale(actualScale, actualScale);
    dc->SetDeviceOrigin( (long)posX, (long)posY );
    // ok,现在开始画画
    dc.SetBackground(*wxWHITE_BRUSH);
    dc.Clear();
    dc.SetFont(wxGetApp().m_testFont);
    dc.SetBackgroundMode(wxTRANSPARENT);
    dc.SetBrush(*wxCYAN_BRUSH);
    dc.SetPen(*wxRED_PEN);
    dc.DrawRectangle(0, 30, 200, 100);
    dc.DrawText( wxT("Rectangle 200 by 100"), 40, 40);
    dc.SetPen( wxPen(*wxBLACK,0,wxDOT_DASH) );
    dc.DrawEllipse(50, 140, 100, 50);
    dc.SetPen(*wxRED_PEN);
    dc.DrawText( wxT("Test message: this is in 10 point text"),
                 10, 180);
}

在上面的例子中,我们只是简单的使用wxDC::GetSize来得到打印设备或者预览图像的分辨率,以便我们能够把要打印的图像放到合适的位置。我们没有关心类似每一个英寸多少个点这样的信息,因为我们不需要画精度很高的文本或者是线段。图片不需要很高的精度,因此我们只是进行简单的缩放以便它能够被合适的放置,不致于太大太小或者越界就可以了。

接下来,我们演示一下怎样进行精度很高的文本或者线段的打印以便看上去它和显示在屏幕上的是一致的。而不是只是进行简单的缩放:

void MyPrintout::DrawPageTwo(wxDC *dc)
{
    // 你可以使用下面的代码来设置打印机以便其可以反应出文本在屏幕上的大小
    // 另外下面的代码还将打印一个5cm长的线段。
    // 首先获得屏幕和打印机上各自的1英寸的逻辑象素个数
    int ppiScreenX, ppiScreenY;
    GetPPIScreen(&ppiScreenX, &ppiScreenY);
    int ppiPrinterX, ppiPrinterY;
    GetPPIPrinter(&ppiPrinterX, &ppiPrinterY);
    // 这个缩放因子用来大概的反应屏幕到实际打印设备的一个缩放
    float scale = (float)((float)ppiPrinterX/(float)ppiScreenX);
    // 现在,我们还需要考虑页面缩放
    // (比如:我们正在作打印预览,用户选择了一个缩放级别)
    int pageWidth, pageHeight;
    int w, h;
    dc->GetSize(&w, &h);
    GetPageSizePixels(&pageWidth, &pageHeight);
    // 如果打印设备的页面大小pageWidth == 当前DC的大小, 就不需要考虑这方面的缩放了
    // 但是它们有可能是不一样的
    // 因此,缩放吧.
    float overallScale = scale * (float)(w/(float)pageWidth);
    dc->SetUserScale(overallScale, overallScale);
    // 现在我们来计算每个逻辑单位有多少个毫米
    // 我们知道1英寸大概是25.4毫米.而ppi
    // 代表的是以英寸为单位的. 因此1毫米就等于ppi/25.4个设备单位
    // 另外我们还需要再除以我们的缩放因子scale
    // (译者注:为什么这里是scale而不是overallScale?)
    // (其实overallScale比scale而言,多了一个预览的缩放)
    // (而在预览的时候,我们是希望线段的长度按照用户设置的比例变化的)
    // 因为我们的设备已经被设置了缩放因子
    // 现在让我们来画一个长度为50mm的L图案
    float logUnitsFactor = (float)(ppiPrinterX/(scale*25.4));
    float logUnits = (float)(50*logUnitsFactor);
    dc->SetPen(* wxBLACK_PEN);
    dc->DrawLine(50, 250, (long)(50.0 + logUnits), 250);
    dc->DrawLine(50, 250, 50, (long)(250.0 + logUnits));
    dc->SetBackgroundMode(wxTRANSPARENT);
    dc->SetBrush(*wxTRANSPARENT_BRUSH);
    dc->SetFont(wxGetApp().m_testFont);
    dc->DrawText(wxT("Some test text"), 200, 300 );
}

在类Unix系统上的GTK+版本上的打印

和Mac OS X以及Windows系统不同,Unix系统没有提供一个标准的API同时支持在屏幕上显示文本图片和在打印机上打印文本和图片。实际上,在类Unix系统中,屏幕显示是通过X11库(被GTK+封装又被wxWidgets封装)实现的,而打印要通过发送PostScript命令到打印机来完成。而这两种情况下使用不同的字体都是一个麻烦。直到最近,才有很少的程序在类Unix上提供了所见即所得的功能。以前wxWidgets提供自己的 PostScript实现,但是它很难和屏幕显示的内容完全一致。

从版本2.8开始,Gnome项目组开始通过libgnomeprint和libgnomeprintui库来提供打印支持,这样大多数的打印问题才算得以解决。从wxWidgets的版本2.5.4开始,GTK+的版本通过合适的配置以后可以支持这两个库。你需要使用--with- gnomeprint来配置wxWidgets,这将导致wxWidgets在运行期自动查找GNOME打印库。如果找的到,就使用它完成打印,否则就使用旧的PostScript打印的代码。需要说明的是,这并不需要用户的机器上一定要安装gnome的打印库程序才可以运行,因为程序本身并不依赖这些库。