如何查找实际可打印区域?(PrintDocument)

49

为什么寻找这个魔术矩形这么难?

在OnPrintPage事件中,我有一个PrintPageEventArgs对象,并且我正在尝试使用Graphics在最大可打印区域的范围内进行绘图。

我已经尝试使用PageBounds、PrintableArea、Graphics.VisibleClipBounds等方法,但是它们都无法始终获取绘图区域,特别是在从横向布局切换到纵向布局时。PrintableArea似乎在从横向布局切换到纵向布局时永远不会改变。

我还注意到,根据我是在进行打印预览还是实际打印,Graphics.VisibleClipBounds的设置方式存在差异。在预览中,它总是显示纵向的宽度/高度,因此我必须检查是否为预览,并在为横向时手动交换宽度/高度。

我需要一种算法来计算可打印区域与当前的Graphics上下文相关联,而不是一个不实际用于绘图的任意理论打印区域。


我的问题是处理Graphics矩阵偏移量。到目前为止,我已经注意到了使用硬边距的Graphics上下文在如下因素影响下的严重不一致性:

  • 如果OriginAtMargins为true或false(行为与我的想法不同)
  • 如果我正在打印到打印机,或者使用PrintPreviewControl(我必须检查这是打印预览还是打印页面,以正确处理翻译)
  • 如果我在家里使用我的打印机还是在工作时使用我的打印机(两者的行为不同)

是否有标准方法来处理这个问题?我应该重新设置矩阵吗?当我将OriginAtMargins设置为true时,Graphics被预先转换为84,84,但我的边距是100,100。硬边距是16,16。它不应该被翻译成100,100吗?因为0,0应该位于页面边界而不是硬边距。

基本上,我的方法应该始终能够获取最佳可打印矩形。我只需要一种一致的、设备无关的方式来确保我的绘图原点(0,0)位于页面左上角,以便上面的矩形对我有任何用处。


1
使用 e.MarginBounds,并将 PrintDocument.OriginAtMargins 设置为 True。 - Hans Passant
我制作了一个简单的项目来测试只使用默认的PrintDocument/PageSetupDialog/PrintDialog,并将OriginAtMargins设置为true,边距设置为0,0,0,0。它似乎可以正常打印到PDF(CutePDFWriter),但是打印到实际打印机时却不行。我用3.0f宽度的笔画了一个矩形,但页面甚至没有打印任何东西(页面只是空白)。我认为这意味着它认为没有任何内容在范围内需要打印。 - Trevor Elliott
如果您想在硬边界处绘制,则最好使用它们。无法在0处打印,那是硬边界内部。如果纸张路径与驱动程序认为的位置有些偏差,则可能仍然会在一侧获取不到任何内容,具体取决于情况。 - Hans Passant
3个回答

83
你的问题有点不太清晰,关于“最好”的矩形具体指什么并没有明确说明。我假设你是指在打印时可以100%显示的最大矩形。
因此,让我们先确保我们理解打印文档图形对象的“原点”以及OriginAtMargins属性如何影响此原点位置。

OriginAtMargins-获取或设置一个值,表示与页面相关联的图形对象的位置是否位于用户指定的页边距内部或页面的可打印区域的左上角。
- MSDN上的PrintDocument Class Definition

如果OriginAtMargins设置为false(默认值),则图形对象将调整为可打印区域矩形(对于我的激光打印机,每个页面边缘约有5/32的空白,旧的激光打印机可能更多,新的喷墨打印机可以打印到边缘,软件PDF打印机将直接打印到边缘)。因此,在我的图形对象中,0,0实际上是在激光打印机的物理页面上的16,16位置(您的打印机可能不同)。

使用默认的1英寸页面边距和OriginAtMargins设置为true,图形对象将调整为100,100,650,1100矩形,适用于普通纵向信纸页面。这是每个物理页面边缘内部的一英寸。因此,在您的图形对象中,0,0实际上是在物理页面上的100,100位置。

边距也称为“软边距”,因为它们在软件中定义,不受物理打印设备的影响。这意味着它们将应用于软件中的当前页面大小,并反映实际的页面尺寸(纵向或横向)。

PrintableArea也被称为“硬边距”,它反映了打印设备的物理限制。这将因打印机和制造商而异。由于这些是硬件测量,所以当您将页面设置为横向/纵向时,它们不会旋转。无论如何更改软件打印设置,打印机上的物理限制都不会改变,因此我们需要根据打印文档(方向)的软件设置,在正确的轴上应用它们。
因此,根据您发布的示例代码的粗略模型,这里是一个PrintDocument.PrintPage事件处理程序,它将绘制一个尽可能大但仍然可见的矩形(默认情况下PrintDocument.OriginsAtMargins为false)。如果将PrintDocument.OriginsAtMargins设置为true,则它将在配置的软边距内绘制一个尽可能大但仍然可见的矩形(默认情况下距页面边缘1英寸)。
PrintAction printAction = PrintAction.PrintToFile;

private void printDocument_BeginPrint(object sender, PrintEventArgs e)
{
    // Save our print action so we know if we are printing 
    // a preview or a real document.
    printAction = e.PrintAction;

    // Set some preferences, our method should print a box with any 
    // combination of these properties being true/false.
    printDocument.OriginAtMargins = false;   //true = soft margins, false = hard margins
    printDocument.DefaultPageSettings.Landscape = false;
}

private void printDocument_PrintPage(object sender, PrintPageEventArgs e)
{
    Graphics g = e.Graphics;

    // If you set printDocumet.OriginAtMargins to 'false' this event 
    // will print the largest rectangle your printer is physically 
    // capable of. This is often 1/8" - 1/4" from each page edge.
    // ----------
    // If you set printDocument.OriginAtMargins to 'false' this event
    // will print the largest rectangle permitted by the currently 
    // configured page margins. By default the page margins are 
    // usually 1" from each page edge but can be configured by the end
    // user or overridden in your code.
    // (ex: printDocument.DefaultPageSettings.Margins)

    // Grab a copy of our "soft margins" (configured printer settings)
    // Defaults to 1 inch margins, but could be configured otherwise by 
    // the end user. You can also specify some default page margins in 
    // your printDocument.DefaultPageSetting properties.
    RectangleF marginBounds = e.MarginBounds;

    // Grab a copy of our "hard margins" (printer's capabilities) 
    // This varies between printer models. Software printers like 
    // CutePDF will have no "physical limitations" and so will return 
    // the full page size 850,1100 for a letter page size.
    RectangleF printableArea = e.PageSettings.PrintableArea;

    // If we are print to a print preview control, the origin won't have 
    // been automatically adjusted for the printer's physical limitations. 
    // So let's adjust the origin for preview to reflect the printer's 
    // hard margins.
    if (printAction == PrintAction.PrintToPreview)
        g.TranslateTransform(printableArea.X, printableArea.Y);

    // Are we using soft margins or hard margins? Lets grab the correct 
    // width/height from either the soft/hard margin rectangles. The 
    // hard margins are usually a little wider than the soft margins.
    // ----------
    // Note: Margins are automatically applied to the rotated page size 
    // when the page is set to landscape, but physical hard margins are 
    // not (the printer is not physically rotating any mechanics inside, 
    // the paper still travels through the printer the same way. So we 
    // rotate in software for landscape)
    int availableWidth = (int)Math.Floor(printDocument.OriginAtMargins 
        ? marginBounds.Width 
        : (e.PageSettings.Landscape 
            ? printableArea.Height 
            : printableArea.Width));
    int availableHeight = (int)Math.Floor(printDocument.OriginAtMargins 
        ? marginBounds.Height 
        : (e.PageSettings.Landscape 
            ? printableArea.Width 
            : printableArea.Height));

    // Draw our rectangle which will either be the soft margin rectangle 
    // or the hard margin (printer capabilities) rectangle.
    // ----------
    // Note: we adjust the width and height minus one as it is a zero, 
    // zero based co-ordinates system. This will put the rectangle just 
    // inside the available width and height.
    g.DrawRectangle(Pens.Red, 0, 0, availableWidth - 1, availableHeight - 1);
}

我认为你在问题中寻找的是确定可用宽度和可用高度的两行代码。这两行代码考虑了软边距或硬边距以及打印文档是横向还是纵向配置。
我使用 Math.Floor() 简单地舍弃小数点后的任何内容(例如:817.96 -> 817),以确保可用宽度和高度刚好在可用尺寸内。我在“安全失败”,如果您想要,可以维护基于浮点数的坐标(而不是整数),只需小心观察会导致裁剪图形的四舍五入误差(如果将817.96四舍五入到818,然后打印机驱动程序决定那不再可见)。
我在 Dell 3115CN、Samsung SCX-4x28 和 CutePDF 软件打印机上测试了此过程,包括横向和纵向以及硬边距和软边距。如果这没有充分解决您的问题,请考虑修改您的问题以澄清“magic rectangle”和“best rectangle”。

编辑说明:“软边距”相关注释

软边距应用于软件中,不考虑打印机的硬件限制。这是有意为之的设计。如果您想要将软边距设置在可打印区域之外,则输出可能会被打印机驱动程序裁剪。如果这对您的应用程序不利,请在程序代码中调整边距。您可以防止用户选择超出可打印区域的边距(或者在他们这样做时警告他们),或者在实际开始打印(绘制)文档时,在您的代码中强制执行一些最小/最大条件。

示例情况:如果您在Microsoft Word 2007中将页面边距设置为0,0,0,0,则会弹出一个警告对话框,显示“一个或多个边距设置在页面的可打印区域之外。选择“修复”按钮以增加相应的边距。”如果您点击修复,Word将简单地将硬边距复制到软边距中,因此对话框现在显示所有边距为0.16英寸(我的激光打印机的能力)。

这是预期的行为。如果用户忽略了警告并使用了0,0,0,0页面边距,那么Microsoft Word打印页面被剪裁不是一个错误/问题。在您的应用程序中也是一样的。您需要根据适当的使用情况强制执行限制。可以通过警告对话框或在代码中更强制地强制执行限制(不提供用户选择)来实现。

备选策略

好的,也许您不仅想获取硬边距,而是想获取软边距,并在打印时强制软边距保持在可打印区域内。让我们开发另一种策略。

在此示例中,我将使用边距作为原点,并允许用户选择任何边距,但我将在代码中强制选择的边距不超出可打印区域。如果所选边距超出了可打印区域,我将简单地调整它们以使其位于可打印区域内。

PrintAction printAction = PrintAction.PrintToFile;

private void printDocument_BeginPrint(object sender, PrintEventArgs e)
{
    // Save our print action so we know if we are printing 
    // a preview or a real document.
    printAction = e.PrintAction;

    // We ALWAYS want true here, as we will implement the 
    // margin limitations later in code.
    printDocument.OriginAtMargins = true;

    // Set some preferences, our method should print a box with any 
    // combination of these properties being true/false.
    printDocument.DefaultPageSettings.Landscape = false;
    printDocument.DefaultPageSettings.Margins.Top = 100;
    printDocument.DefaultPageSettings.Margins.Left = 0;
    printDocument.DefaultPageSettings.Margins.Right = 50;
    printDocument.DefaultPageSettings.Margins.Bottom = 0;
}

private void printDocument_PrintPage(object sender, PrintPageEventArgs e)
{
    Graphics g = e.Graphics;

    // If you set printDocumet.OriginAtMargins to 'false' this event 
    // will print the largest rectangle your printer is physically 
    // capable of. This is often 1/8" - 1/4" from each page edge.
    // ----------
    // If you set printDocument.OriginAtMargins to 'false' this event
    // will print the largest rectangle permitted by the currently 
    // configured page margins. By default the page margins are 
    // usually 1" from each page edge but can be configured by the end
    // user or overridden in your code.
    // (ex: printDocument.DefaultPageSettings.Margins)

    // Grab a copy of our "hard margins" (printer's capabilities) 
    // This varies between printer models. Software printers like 
    // CutePDF will have no "physical limitations" and so will return 
    // the full page size 850,1100 for a letter page size.
    RectangleF printableArea = e.PageSettings.PrintableArea;
    RectangleF realPrintableArea = new RectangleF(
        (e.PageSettings.Landscape ? printableArea.Y : printableArea.X),
        (e.PageSettings.Landscape ? printableArea.X : printableArea.Y),
        (e.PageSettings.Landscape ? printableArea.Height : printableArea.Width),
        (e.PageSettings.Landscape ? printableArea.Width : printableArea.Height)
        );

    // If we are printing to a print preview control, the origin won't have 
    // been automatically adjusted for the printer's physical limitations. 
    // So let's adjust the origin for preview to reflect the printer's 
    // hard margins.
    // ----------
    // Otherwise if we really are printing, just use the soft margins.
    g.TranslateTransform(
        ((printAction == PrintAction.PrintToPreview) 
            ? realPrintableArea.X : 0) - e.MarginBounds.X,
        ((printAction == PrintAction.PrintToPreview) 
            ? realPrintableArea.Y : 0) - e.MarginBounds.Y
    );

    // Draw the printable area rectangle in PURPLE
    Rectangle printedPrintableArea = Rectangle.Truncate(realPrintableArea);
    printedPrintableArea.Width--;
    printedPrintableArea.Height--;
    g.DrawRectangle(Pens.Purple, printedPrintableArea);

    // Grab a copy of our "soft margins" (configured printer settings)
    // Defaults to 1 inch margins, but could be configured otherwise by 
    // the end user. You can also specify some default page margins in 
    // your printDocument.DefaultPageSetting properties.
    RectangleF marginBounds = e.MarginBounds;

    // This intersects the desired margins with the printable area rectangle. 
    // If the margins go outside the printable area on any edge, it will be 
    // brought in to the appropriate printable area.
    marginBounds.Intersect(realPrintableArea);

    // Draw the margin rectangle in RED
    Rectangle printedMarginArea = Rectangle.Truncate(marginBounds);
    printedMarginArea.Width--;
    printedMarginArea.Height--;
    g.DrawRectangle(Pens.Red, printedMarginArea);
}

1
我真正想要的只是可打印的矩形。对于让边距对称打印,这只是事后我要做的事情。那不是真正的问题。我在寻找的是一个通用算法,能给我提供一个可以打印的矩形,并考虑到不同打印机等各种情况下的边界情况。 - Trevor Elliott
此外,您的方法无法使用PrintPreviewControl显示预览。我不知道这是否只是该控件的问题,但您需要检查并通过硬边距偏移绘图来模拟打印机,否则它会打印超出页面。 - Trevor Elliott
@Moozhe 上述代码完全按照你的要求执行,对于所有情况都获取可打印的矩形(硬边界根据方向进行调整)。同时将边距设置为0,0,0,0会按照定义行事(所期望的结果)。你需要在代码中防止用户选择低于硬边界的边距,或者事后转换边距。我认为如果你在问题中编辑一个更清晰的示例用例,我可能能够更直接地回答它,而不是提供一个通用的示例。但是上面的示例代码确实能够通用地获取硬边界。 - BenSwayne
我认为上述确定实际打印区域的方法在某些情况下略有偏差。当我选择一个没有边距的PostScript打印机时,最终得到的矩形会向左移动1/100英寸。我认为使用相交方法是导致问题的原因。 - yu_ominae
"100,100,650,1100" 你的意思是900而不是1100。 - Dave Cousineau
显示剩余2条评论

3

目前我的打印机可以正常工作。我将OriginAtMargins设置为false。这会导致在我打印到打印机时自动转换为HardMarginX和HardMarginY,但在我打印到PrintPreviewControl时不进行转换。因此,我必须检查这种情况。

private void printDocument_BeginPrint(object sender, PrintEventArgs e)
{
    printAction = e.PrintAction;
    printDocument.OriginAtMargins = false;
}

private void printDocument_PrintPage(object sender, PrintPageEventArgs e)
{
    Graphics g = e.Graphics;

    if (printAction != PrintAction.PrintToPreview)
        g.TranslateTransform(-e.PageSettings.HardMarginX, -e.PageSettings.HardMarginY);

    RectangleF printArea = GetBestPrintableArea(e);

    g.DrawRectangle(Pens.Red, printArea.X, printArea.Y, printArea.Width - 1, printArea.Height - 1);
}

public RectangleF GetBestPrintableArea(PrintPageEventArgs e)
{
    RectangleF marginBounds = e.MarginBounds;
    RectangleF printableArea = e.PageSettings.PrintableArea;
    RectangleF pageBounds = e.PageBounds;

    if (e.PageSettings.Landscape)
        printableArea = new RectangleF(printableArea.Y, printableArea.X, printableArea.Height, printableArea.Width);

    RectangleF bestArea = RectangleF.FromLTRB(
        (float)Math.Max(marginBounds.Left, printableArea.Left),
        (float)Math.Max(marginBounds.Top, printableArea.Top),
        (float)Math.Min(marginBounds.Right, printableArea.Right),
        (float)Math.Min(marginBounds.Bottom, printableArea.Bottom)
    );

    float bestMarginX = (float)Math.Max(bestArea.Left, pageBounds.Right - bestArea.Right);
    float bestMarginY = (float)Math.Max(bestArea.Top, pageBounds.Bottom - bestArea.Bottom);

    bestArea = RectangleF.FromLTRB(
        bestMarginX,
        bestMarginY,
        pageBounds.Right - bestMarginX,
        pageBounds.Bottom - bestMarginY
    );

    return bestArea;
}

如果有人能够在他们的打印机上尝试此代码以验证其通用性,或者如果我错了,那将是非常好的。我不知道当OriginAtMargins为false时,原点是否会预先转换为硬边界对于所有打印机都是标准的,还是只有我的打印机会这样做。

这段代码对于我理解的问题来说有点过于复杂了。你根本不需要对图形对象进行TranslateTransform操作(它已经为你完成了),而且我也不会将软边距和硬边距合并在一起(marginBounds和printableArea)。我会选择使用软边距或硬边距中的一个来处理。请参见我的答案以获取示例和澄清。 - BenSwayne
我按照您的要求测试了您的代码。它与我在下面答案中提供的代码示例相同。我在物理打印输出上得到了硬边距,但在打印预览上没有。我将修改我的答案,以包括针对打印预览和物理打印输出的调整。我仍然认为您可以更优雅地实现这一点。 - BenSwayne

0

我认为你需要做的就是重新绘制图像以适应所使用的任何纸张尺寸。这是我的代码:

Protected Overrides Sub OnPrintPage(ByVal e As System.Drawing.Printing.PrintPageEventArgs)
        Dim img As Image = Nothing 'Your image source

        Dim ps As PaperSize = MyBase.PrinterSettings.DefaultPageSettings.PaperSize
        Dim pF As RectangleF = MyBase.PrinterSettings.DefaultPageSettings.PrintableArea
        Dim srcF As New RectangleF(0, 0, pg.ImageSize.Width, pg.ImageSize.Height)
        Dim dstF As New RectangleF(0, 0, pF.Width, pF.Height)

        e.Graphics.InterpolationMode = Drawing2D.InterpolationMode.HighQualityBicubic
        e.Graphics.DrawImage(img, dstF, srcF, GraphicsUnit.Pixel)

        MyBase.OnPrintPage(e)
End Sub

这是一个过于复杂和错误的例子,试图表达“绘制到可打印区域”的意思,我已经尝试过了。它没有考虑边距或方向。 - Trevor Elliott
3
OP将问题标记为 C#。虽然 VBC# 相似,但请只提供 C# 代码。 - L. Guthardt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接