指定GDI设备上下文的DPI

12

我有一个生成meta文件(EMF)的应用程序。它使用参考设备(即屏幕)来渲染这些meta文件,因此meta文件的DPI取决于代码运行在哪台机器上。

假设我的代码要创建一个8.5英寸x 11英寸的meta文件。使用我的开发工作站作为参考,我得到了一个EMF,其中

  • rclFrame是{0、0、21590,27940}(以千分之一毫米为单位的meta文件尺寸)
  • szlDevice是{1440、900}(以像素为单位的参考设备尺寸)
  • szlMillimeters是{416、260}(以毫米为单位的参考设备尺寸)

好的,rclFrame告诉我EMF的大小应该是

  • 21590/2540=8.5英寸宽
  • 27940/2540=11英寸高

很好。如果我的数学没错,使用这些信息,我们还可以确定我的显示器的物理DPI:

  • (1440 * 25.4)/ 416 = 87.9231水平dpi
  • (900 * 25.4)/ 260 = 87.9231垂直dpi

问题

任何播放此meta文件的内容(例如EMF到PDF转换,右键单击Windows资源管理器中的“摘要”页面等)似乎都会截断计算出来的DPI值,显示87而不是87.9231(即使显示88也可以)。

当播放meta文件时,这导致页面的物理尺寸为8.48英寸x 10.98英寸(使用87 dpi),而不是8.5英寸x 11英寸(使用88 dpi)。

  • 是否可以更改参考设备的DPI,以便存储在用于计算DPI的meta文件中的信息变为一个漂亮的整数?
  • 我可以创建自己的设备上下文并指定其 DPI 吗?还是一定要使用打印机来实现?
  • 感谢任何见解。


    摘要页面上的尺寸显示正确吗?是748x968还是747x967? - Vanuan
    你在这个问题中所展示的数学推导对我来说价值无法估量。 - bkwdesign
    4个回答

    17

    我现在已经学到比我想知道的更多关于元文件(metafile)了。

    1. Metafile类的一些构造函数重载效果不佳,会使用截断的DPI值进行操作。

    考虑以下内容:

    protected Graphics GetNextPage(SizeF pageSize)
    {
        IntPtr deviceContextHandle;
        Graphics offScreenBufferGraphics;
        Graphics metafileGraphics;
        MetafileHeader metafileHeader;
    
        this.currentStream = new MemoryStream();
        using (offScreenBufferGraphics = Graphics.FromHwnd(IntPtr.Zero))
        {
            deviceContextHandle = offScreenBufferGraphics.GetHdc();
            this.currentMetafile = new Metafile(
                this.currentStream,
                deviceContextHandle,
                new RectangleF(0, 0, pageSize.Width, pageSize.Height),
                MetafileFrameUnit.Inch,
                EmfType.EmfOnly);
    
            metafileGraphics = Graphics.FromImage(this.currentMetafile);
    
            offScreenBufferGraphics.ReleaseHdc();
        }
    
        return metafileGraphics;
    }
    
    如果您传递了大小为 { 8.5, 11 } 的 SizeF,您可能希望得到一个具有 rclFrame { 21590, 27940 } 的 Metafile。毕竟,将英寸转换为毫米并不难。但实际上可能不会这样。根据您的分辨率,GDI+在转换英寸参数时似乎会使用截断的 DPI 值。为了做到正确,我必须以百分之一毫米的方式自己完成,因为 GDI+ 只是通过元文件头原生存储百分之一毫米来传递它。
    this.currentMetafile = new Metafile(
        this.currentStream,
        deviceContextHandle,
        new RectangleF(0, 0, pageSize.Width * 2540, pageSize.Height * 2540),
        MetafileFrameUnit.GdiCompatible,
        EmfType.EmfOnly);
    

    解决了舍入误差问题——我的元文件的rclFrame现在是正确的。

    2. 绘制到Metafile实例的Graphics对象上时,DPI总是错误的。

    看到我通过对元文件调用Graphics.FromImage()设置的 metafileGraphics 变量吗?嗯,好像这个Graphics实例始终具有96 DPI。(如果要猜测,它始终设置为逻辑DPI而不是物理DPI。)

    当您在一个操作96 DPI并记录到其标题中具有87.9231 DPI“记录”的Metafile实例上的Graphics对象上进行绘制时,可能会发生一些滑稽的情况(请记住,存储在元文件中的GDI命令是以像素为单位指定的)。元文件的“像素”更大,所以你会诅咒和嘟囔,为什么你绘制一英寸长的东西最终变成超出一英寸的长度。

    解决方法是缩小Graphics实例:

    
    metafileGraphics = Graphics.FromImage(this.currentMetafile);
    metafileHeader = this.currentMetafile.GetMetafileHeader();
    metafileGraphics.ScaleTransform(
        metafileHeader.DpiX / metafileGraphics.DpiX,
        metafileHeader.DpiY / metafileGraphics.DpiY);
    
    这不是件很有趣的事吗?但它似乎有效。
    “舍入”误差#2已解决——当我说要在88 dpi下绘制“1英寸”的东西时,该像素最好被记录为像素#88。
    szlMillimeters可能会有很大变化;远程桌面带来了很多乐趣。所以我们发现(根据马克的答案),有时Windows会查询您的显示器的EDID,并实际上知道其物理大小。GDI+在填写szlMillimeters属性时会用到这个信息(HORZSIZE等)。现在想象一下,你回家调试这个远程桌面的代码。假设你的家用电脑碰巧有一个16:9的宽屏显示器。显然,Windows无法查询远程显示器的EDID。因此,它使用古老的默认值320 x 240 mm,这本来没问题,但它碰巧是4:3的长宽比,现在完全相同的代码正在产生一个元文件,在一个被认为具有非正方形物理像素的显示器上:水平DPI和垂直DPI是不同的,我记不得上次看到这种情况是什么时候了。我的临时解决方法是:“好吧,不要在远程桌面下运行它。”
    我使用的EMF-to-PDF工具在查看rclFrame标头时存在舍入误差。这是导致我问题的主要原因,触发了这个问题。我的元文件一直是“正确”的(在我修复前两个问题后),所有寻找创建“高分辨率”元文件的搜索都是一个误导。当在低分辨率显示设备上记录元文件时,可能会丢失一些保真度;这是因为元文件中指定的GDI命令是以像素为单位指定的。无论它是矢量格式并可以放大或缩小,有些信息在GDI+决定将操作捕捉到哪个“像素”时“在实际记录期间”丢失了。
    我联系了供应商他们给了我一个更正版本,解决了舍入错误#3。
    Windows Explorer中的“摘要”面板恰好在显示计算DPI时截断值。除此之外,这种怪癖对讨论没有任何有意义的贡献。
    结论:由于我的问题与设备上下文中的DPI有关,马克的答案是一个好答案。

    请注意,您可以更改Windows监视器配置,使96 DPI不再是逻辑DPI。但这可能仅在XP上造成问题,因为他们为Vista更改了一些东西。而且,使用GDI+会进一步改变事情。请访问http://msdn.microsoft.com/en-us/library/ms701681(VS.85).aspx。 - Mark Ransom

    3
    我很好奇Windows如何知道您的显示器的物理尺寸。您一定在某个地方更改了配置吗?也许您可以将其更改为更方便的值,以便更好地分割。
    顾名思义,“设备上下文”必须连接到系统设备。但是这不需要是硬件驱动程序,它可以是设备仿真器,例如PDF编写器打印驱动程序。我至少见过一个可以让您设置任意DPI的驱动程序。

    1
    Windows不知道显示器的物理尺寸,但是你可以从EDID获取它,参见https://dev59.com/9XRB5IYBdhLWcg3wl4Kc。我的理解是,如果你将显示器设置为其本机分辨率,如果它是平板电视,你几乎总是会得到(非常接近)96像素每英寸。 - Robert Harvey
    1
    嗯,那很有趣。这似乎表明 .NET 的 Metafile 类构造函数调用的 GdipRecordMetafileStream 正在计算它。 (我的显示器分辨率是其本机分辨率 - Dell E198WFP - 我拿出了卷尺,那些毫米尺寸确实正确!) - Nicholas Piasecki

    0

    看起来摘要页面上的值是错误的。它们被计算为:

    Size = round(precize_size)+1
    Resolution = trunc(precize_resolution)
    

    在计算时精确计算数值,不进行四舍五入或截断。


    0
    请注意,我一直在WXP上以120 dpi(大字体)运行,这意味着metafileGraphics.DpiX将返回120。
    EMF文件似乎没有记录参考上下文的dpi是多少(在这种情况下为120,在大多数其他人中为96)。
    更有趣的是,可以通过绘制到内存位图上并将SetResolution()设置为300dpi来创建EMF。在这种情况下,我认为缩放因子必须是300,而不是显示器(86.x)或Windows(120)可能正在使用的因子。

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