将图形复制到屏幕尺寸之外的TMetaFileCanvas上

7
我们在将图像绘制到屏幕分辨率之外的坐标时,使用TMetaFileCanvas输出会出现问题。矢量操作似乎没有问题,但图像操作却被“忽略”了。如果我们将相同的图像绘制到屏幕范围内的坐标,则不会出现任何问题。
例如,这个SSCCE将生成4个输出文件。位图变体没有问题,并且将在inscreen.bmp的左上角和outsidescreen.bmp的右下角输出预期的红色正方形。inscreen.emf元文件按预期工作,将红色正方形绘制在左上角。但是outsidescreen.emf无法正常工作,只绘制了一条线。
program Project6;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.Types,
  Windows,
  Vcl.Graphics;

const
  SIZECONST = 3000; // should be larger than your screen resolution
  OFFSET = 1500;

  function GetMyMetafile(const aHDC: HDC): TMetafile;
  var
    metcnv: TMetafileCanvas;
  begin
    Result := TMetafile.Create;
    Result.SetSize(500, 500);

    metcnv := TMetafileCanvas.Create(Result, aHDC);
    metcnv.Brush.Color := clRed;
    metcnv.FillRect(Rect(0, 0, 500, 500));
    metcnv.Free;
  end;

  procedure OutputToMetaFile(const aFilename: string; const aStartOffset,
      aEndOffset, aMaxSize: Integer; aGraphic: TGraphic; aHDC: HDC);
  var
    metafile: TMetafile;
    metcnv: TMetafileCanvas;
  begin
    metafile := TMetafile.Create;
    try
      metafile.SetSize(aMaxSize, aMaxSize);

      metcnv := TMetafileCanvas.Create(metafile, aHDC);
      try
        // draw it somewhere offscreen
        metcnv.StretchDraw(Rect(aStartOffset, aStartOffset, aEndOffset, aEndOffset), aGraphic);
        metcnv.MoveTo(aStartOffset, aStartOffset);
        metcnv.LineTo(aEndOffset, aEndOffset);
      finally
        metcnv.Free;
      end;

      metafile.SaveToFile(aFilename);
    finally
      metafile.Free;
    end;
  end;

  procedure OutputToBitmap(const aFilename: string; const aStartOffset,
      aEndOffset, aMaxSize: Integer; aGraphic: TGraphic);
  var
    bmp: TBitmap;
  begin
    bmp := TBitmap.Create;
    try
      bmp.SetSize(aMaxSize, aMaxSize);

      bmp.Canvas.StretchDraw(Rect(aStartOffset, aStartOffset, aEndOffset, aEndOffset), aGraphic);
      bmp.Canvas.MoveTo(aStartOffset, aStartOffset);
      bmp.Canvas.LineTo(aEndOffset, aEndOffset);

      bmp.SaveToFile(aFilename);
    finally
      bmp.Free;
    end;
  end;

var
  mygraph: TMetafile;
  bigBitmap: TBitmap;
begin
  bigBitmap := TBitmap.Create;
  try
    bigBitmap.PixelFormat := pf24bit;
    Assert(bigBitmap.HandleType = bmDIB, 'Handle Type should be a DIB');
    bigBitmap.Width := SIZECONST;
    bigBitmap.Height := SIZECONST;
    mygraph := GetMyMetafile(bigBitmap.Canvas.Handle);
    OutputToMetaFile('inscreen.emf', 0, 1000, SIZECONST, mygraph, bigBitmap.Canvas.Handle);
    OutputToMetaFile('outsidescreen.emf', OFFSET, SIZECONST-1, SIZECONST, mygraph, bigBitmap.Canvas.Handle);

    // do the same using bitmap
    OutputToBitmap('inscreen.bmp', 0, 1000, SIZECONST, mygraph);
    OutputToBitmap('outsidescreen.bmp', OFFSET, SIZECONST-1, SIZECONST, mygraph);
  finally
    bigBitmap.Free;
    mygraph.Free;
  end;
end.

有人能看到问题吗?或者你知道一个解决方法吗?

更新

我在最初提问时应该包含这个信息。我们已经使用HDC测试了一个大位图,而且它也出现了同样的问题。我已经更新了示例代码以演示这一点。

更新2

不幸的是,即使有了赏金,解决方案仍然难以捉摸。在屏幕大小之外的任何BitBlt操作都不会被绘制。

当图像处于屏幕坐标范围内时,以下是元文件操作的提取:

R0001: [001] EMR_HEADER (s=108) {{ Bounds(500,500,18138,18129), Frame(0,0,105000,105000), ver(0x10000), size(688), recs(33), handles(2), pals(0), dev_pix(1080,1920), dev_mil(381,677), pixf_size(0), pixf_ofs(0), openGL(0) }}
R0002: [033] EMR_SAVEDC (s=8)
R0003: [115] EMR_SETLAYOUT  (s=12)  {iMode(0=<default>)}
R0004: [028] EMR_SETMETARGN (s=8)
R0005: [037] EMR_SELECTOBJECT   (s=12)  {Stock object: 0=OBJ_BRUSH.(BS_SOLID)}
R0006: [037] EMR_SELECTOBJECT   (s=12)  {Stock object: 7=OBJ_PEN.(PS_SOLID | COSMETIC)}
R0007: [037] EMR_SELECTOBJECT   (s=12)  {Stock object: 14=OBJ_FONT}
R0008: [025] EMR_SETBKCOLOR (s=12)  {0x00FFFFFF}
R0009: [024] EMR_SETTEXTCOLOR   (s=12)  {0x00000000}
R0010: [018] EMR_SETBKMODE  (s=12)  {iMode(2=OPAQUE)}
R0011: [019] EMR_SETPOLYFILLMODE    (s=12)  {iMode(1=ALTERNATE)}
R0012: [020] EMR_SETROP2    (s=12)  {iMode(13=R2_COPYPEN)}
R0013: [021] EMR_SETSTRETCHBLTMODE  (s=12)  {iMode(1=BLACKONWHITE)}
R0014: [022] EMR_SETTEXTALIGN   (s=12)  {iMode(0= TA_LEFT TA_TOP)}
R0015: [013] EMR_SETBRUSHORGEX  (s=16)  {ptlOrigin(0,0)}
R0016: [058] EMR_SETMITERLIMIT  (s=12)  {Limit:0.000}
R0017: [027] EMR_MOVETOEX   (s=16)  { ptl(0,0)}
R0018: [035] EMR_SETWORLDTRANSFORM  (s=32)  {xform(eDx:500.000000, eDy:500.000000, eM11:5.039683, eM12:0.000000, eM21:0.000000, eM22:5.037203)}
R0019: [036] EMR_MODIFYWORLDTRANSFORM   (s=36)  {iMode(4=MWT_??), xform(eDx:500.000000, eDy:500.000000, eM11:5.039683, eM12:0.000000, eM21:0.000000, eM22:5.037203)}
R0020: [115] EMR_SETLAYOUT  (s=12)  {iMode(0=<default>)}
R0021: [070] EMR_GDICOMMENT (s=40)  {GDI.Begin Group}
R0022: [039] EMR_CREATEBRUSHINDIRECT    (s=24)  {ihBrush(1), style(0=BS_SOLID, color:0x000000FF)}
R0023: [037] EMR_SELECTOBJECT   (s=12)  {Table object: 1=OBJ_BRUSH.(BS_SOLID)}
R0024: [037] EMR_SELECTOBJECT   (s=12)  {Table object: 1=OBJ_BRUSH.(BS_SOLID)}
R0025: [076] EMR_BITBLT (s=100) {rclBounds(500,500,18138,18129), Dest[x:0, y:0, cx:3500, cy:3500)], dwRop(0x00F00021), Src[x:0, y:0, xform(eDx:0.000000, eDy:0.000000, eM11:1.000000, eM12:0.000000, eM21:0.000000, eM22:1.000000), BkColor:0x00000000, iUsage:0, offBmi:0, Bmi:0, offBits:0, Bits:0]}
R0026: [037] EMR_SELECTOBJECT   (s=12)  {Table object: 1=OBJ_BRUSH.(BS_SOLID)}
R0027: [037] EMR_SELECTOBJECT   (s=12)  {Stock object: 0=OBJ_BRUSH.(BS_SOLID)}
R0028: [040] EMR_DELETEOBJECT   (s=12)  {ihObject(1)}
R0029: [070] EMR_GDICOMMENT (s=20)  {GDI.End Group}
R0030: [034] EMR_RESTOREDC  (s=12)  {iRelative(-1)}
R0031: [027] EMR_MOVETOEX   (s=16)  { ptl(500,500)}
R0032: [054] EMR_LINETO (s=16)  { ptl(1000,1000)}
R0033: [014] EMR_EOF    (s=20)  {nPalEntries:0, offPalEntries:16, nSizeLast:20}

以下是当图像超出屏幕坐标范围时的 Metafile 操作摘录:
R0001: [001] EMR_HEADER (s=108) {{ Bounds(1500,1500,2999,2999), Frame(0,0,105000,105000), ver(0x10000), size(588), recs(32), handles(2), pals(0), dev_pix(1080,1920), dev_mil(381,677), pixf_size(0), pixf_ofs(0), openGL(0) }}
R0002: [033] EMR_SAVEDC (s=8)
R0003: [115] EMR_SETLAYOUT  (s=12)  {iMode(0=<default>)}
R0004: [028] EMR_SETMETARGN (s=8)
R0005: [037] EMR_SELECTOBJECT   (s=12)  {Stock object: 0=OBJ_BRUSH.(BS_SOLID)}
R0006: [037] EMR_SELECTOBJECT   (s=12)  {Stock object: 7=OBJ_PEN.(PS_SOLID | COSMETIC)}
R0007: [037] EMR_SELECTOBJECT   (s=12)  {Stock object: 14=OBJ_FONT}
R0008: [025] EMR_SETBKCOLOR (s=12)  {0x00FFFFFF}
R0009: [024] EMR_SETTEXTCOLOR   (s=12)  {0x00000000}
R0010: [018] EMR_SETBKMODE  (s=12)  {iMode(2=OPAQUE)}
R0011: [019] EMR_SETPOLYFILLMODE    (s=12)  {iMode(1=ALTERNATE)}
R0012: [020] EMR_SETROP2    (s=12)  {iMode(13=R2_COPYPEN)}
R0013: [021] EMR_SETSTRETCHBLTMODE  (s=12)  {iMode(1=BLACKONWHITE)}
R0014: [022] EMR_SETTEXTALIGN   (s=12)  {iMode(0= TA_LEFT TA_TOP)}
R0015: [013] EMR_SETBRUSHORGEX  (s=16)  {ptlOrigin(0,0)}
R0016: [058] EMR_SETMITERLIMIT  (s=12)  {Limit:0.000}
R0017: [027] EMR_MOVETOEX   (s=16)  { ptl(0,0)}
R0018: [035] EMR_SETWORLDTRANSFORM  (s=32)  {xform(eDx:1500.000000, eDy:1500.000000, eM11:15.108969, eM12:0.000000, eM21:0.000000, eM22:15.101533)}
R0019: [036] EMR_MODIFYWORLDTRANSFORM   (s=36)  {iMode(4=MWT_??), xform(eDx:1500.000000, eDy:1500.000000, eM11:15.108969, eM12:0.000000, eM21:0.000000, eM22:15.101533)}
R0020: [115] EMR_SETLAYOUT  (s=12)  {iMode(0=<default>)}
R0021: [070] EMR_GDICOMMENT (s=40)  {GDI.Begin Group}
R0022: [039] EMR_CREATEBRUSHINDIRECT    (s=24)  {ihBrush(1), style(0=BS_SOLID, color:0x000000FF)}
R0023: [037] EMR_SELECTOBJECT   (s=12)  {Table object: 1=OBJ_BRUSH.(BS_SOLID)}
R0024: [037] EMR_SELECTOBJECT   (s=12)  {Table object: 1=OBJ_BRUSH.(BS_SOLID)}
R0025: [037] EMR_SELECTOBJECT   (s=12)  {Table object: 1=OBJ_BRUSH.(BS_SOLID)}
R0026: [037] EMR_SELECTOBJECT   (s=12)  {Stock object: 0=OBJ_BRUSH.(BS_SOLID)}
R0027: [040] EMR_DELETEOBJECT   (s=12)  {ihObject(1)}
R0028: [070] EMR_GDICOMMENT (s=20)  {GDI.End Group}
R0029: [034] EMR_RESTOREDC  (s=12)  {iRelative(-1)}
R0030: [027] EMR_MOVETOEX   (s=16)  { ptl(1500,1500)}
R0031: [054] EMR_LINETO (s=16)  { ptl(2999,2999)}
R0032: [014] EMR_EOF    (s=20)  {nPalEntries:0, offPalEntries:16, nSizeLast:20}

你可以清楚地看到,在第一个操作中缺少BilBlt操作(R0025)。
2个回答

6
您正在创建 TMetaFileCanvas,并将参数ReferenceDevice设置为0,因此它会将ReferenceDevice设置为GetDC(0)HDC,即屏幕。 ReferenceDevice用于获取在EMF绘制期间使用的分辨率和功能。例如,当TMetaFile的尺寸为空时,TMetaFileCanvas使用ReferenceDevice的尺寸。然后,TMetaFileCanvas会为自己创建一个HDC,其边界矩形基于TMetaFileReferenceDevice中可用的尺寸。
因此,要解决问题,请提供足够大的ReferenceDevice来处理您的绘图。在创建TMetaFileCanvas之前,您可以预先调整TMetaFile的尺寸以匹配所需的最大尺寸,但您可能需要创建所需最大尺寸的TBitmap并使用其Canvas.Handle作为ReferenceDevice,而不是使用屏幕。
在内部,TCanvas.StretchDraw()只调用TGraphic.Draw()TMetaFile.Draw() "播放"元文件到目标画布的HDC上。当HDC是由TMetaFileCanvas创建的时,您无法在分配给TMetaFileCanvas的尺寸之外绘制。

我们已经尝试过了。我应该在问题中包含它。我将使用一个大位图的代码示例更新问题,以展示相同的问题。 - Graymatter
此答案获得悬赏金。我认为答案可能在需要创建的参考设备中,或者我们必须等待Windows修复此问题。 - Graymatter

2
program Project1;
{$APPTYPE CONSOLE}

uses
 SysUtils, Types, Windows, Graphics;

const
 SIZECONST = 3000; // should be larger than your screen resolution
 OFFSET = 1500;
var
 //holds millimeter per pixel ratios
 MMPerPixelHorz,
 MMPerPixelVer: Integer;

 procedure CreateMyMetafile(var HmyGraphic: HENHMETAFILE; aHDC: HDC);
 var
  R: Trect;
  TheBrush: HBRUSH;
  OldBrush: HBRUSH;
  MetafileDC: HDC;
begin
  R:= Rect(0, 0, 100*MMPerPixelHorz, 100*MMPerPixelVer);
  MetafileDC:= CreateEnhMetaFile(aHDC, 'myGraphic.emf', @R, nil);

  TheBrush:=CreateSolidBrush(RGB(255, 0, 0));
  OldBrush:=SelectObject(MetafileDC, TheBrush);

  Rectangle(MetafileDC, r.Left, r.Top, r.Right, r.Bottom);

  SelectObject(MetafileDC, OldBrush);
  DeleteObject(TheBrush);
  HmyGraphic:=CloseEnhMetaFile(MetafileDC);
end;

procedure OutputToMetaFile(const aFilename: string; const aStartOffset,
     aEndOffset, aMaxSize: Integer; aHDC: HDC);
var
  r: Trect;

 ReferenceRect: TRect;
 MetafileDC: HDC;
 HMetaFile, HMetaMyGraphic: HENHMETAFILE; {EMF file handle}
begin
 //create our reference rectangle for the metafile
 ReferenceRect:= Rect(0, 0, aMaxSize * MMPerPixelHorz, aMaxSize * MMPerPixelVer);

 //Create First EnhMetaFile
 CreateMyMetafile(HMetaMyGraphic, aHDC);

 MetafileDC:=CreateEnhMetaFile(aHDC, pchar(aFilename),@ReferenceRect, nil);
 //SetMapMode(MetafileDC, MM_ANISOTROPIC);

 try
   r:= Rect(aStartOffset, aStartOffset, aEndOffset, aEndOffset);
   PlayEnhMetaFile (MetaFileDC, HMetaMyGraphic, r);

   MoveToEx(MetafileDC, aStartOffset, aStartOffset, nil);
   LineTo(MetafileDC, aEndOffset, aEndOffset);
   HMetaFile:=CloseEnhMetaFile(MetafileDC);

 finally
   DeleteEnhMetaFile (HMetaFile);
   DeleteEnhMetaFile (HMetaMyGraphic);
 end;
end;

var
 WidthInMM,
 HeightInMM,
 WidthInPixels,
 HeightInPixels: Integer;

 bigBitmap: TBitmap;
 mHDC: HDC;
begin
  bigBitmap := TBitmap.Create;
  try
    bigBitmap.PixelFormat := pf24bit;
    Assert(bigBitmap.HandleType = bmDIB, 'Handle Type should be a DIB');
    bigBitmap.Width := SIZECONST;
    bigBitmap.Height := SIZECONST;
    mHDC:= bigBitmap.Canvas.Handle;

   //retrieve the size of the screen in millimeters
    WidthInMM:=GetDeviceCaps(mHDC, HORZSIZE);
    HeightInMM:=GetDeviceCaps(mHDC, VERTSIZE);

   //retrieve the size of the screen in pixels
   WidthInPixels:=GetDeviceCaps(mHDC, HORZRES);
   HeightInPixels:=GetDeviceCaps(mHDC, VERTRES);

   MMPerPixelHorz:=(WidthInMM * 100) div WidthInPixels;
   MMPerPixelVer:=(HeightInMM * 100) div HeightInPixels;

   OutputToMetaFile('inscreen.emf', 500, 1000, SIZECONST, mHDC);
   OutputToMetaFile('outsidescreen.emf', OFFSET, SIZECONST-1, SIZECONST, mHDC);
 finally
   bigBitmap.Free;
 end;
end.

谢谢,这个答案看起来是正确的。我们正在仔细检查一些内容,确认无误后我会立即标记这个答案。 - Graymatter
1
好的,这个不正常工作。它仍然使用向量操作,因此问题仍然存在。原始问题在GetMyMetaFile中使用了FillRect,这会在元文件中生成BitBlt操作。而这段代码在CreateMyMetafile中使用了Rectangle,这是一个矢量操作。 - Graymatter
据我所知的赏金规则,如果答案获得了2个或更多的投票,那么一半的赏金将自动授予。显然,这个答案被那些没有注意到它没有解决问题的人们点赞了。应该采取的自然行动是进行负面评价或撤销投票... - Sertac Akyuz
@SertacAkyuz 我将把悬赏给Remy的回答。这可能是Windows的一个错误,或者答案是创建一个特殊的ReferenceDevice。虽然我不会接受这个答案。希望将来有人能够回答这个问题。 - Graymatter
@Gray - 这是一个bug。如果你从上面拉伸你的光栅图像,它会很好地延伸到不应该延伸的地方(根据Remy的说法)。在你问题的代码中,将偏移量设为1080,你就有了图像,将其设为1090,你就没有了。Remy的答案没有实质性内容。 - Sertac Akyuz
@SertacAkyuz 两个答案都不正确,但我必须在某处颁发奖金。我之所以授予Remy是因为如果您使用打印机参考设备,它会根据打印机画布的大小而工作,因此它取决于参考设备的大小。问题在于Windows以某种方式从DIB中捕获屏幕,这是错误的。如果我可以获得另一个不基于屏幕的参考设备,那么我们就可以做生意了。Remy的答案指出了问题的一部分,但他创建位图的建议是错误的。 - Graymatter

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