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

GDI 绘图和打印

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (26投票s)

2014 年 4 月 24 日

Ms-PL

8分钟阅读

viewsIcon

66110

downloadIcon

4635

了解如何在屏幕和打印机上使用 GDI 进行绘图的基础知识。我们将仔细研究不同的 GDI 映射模式,以及在需要将输出发送到打印机时如何进行适当的调整。我们还将创建一个图元文件并将其重新加载。

Screenshot of demo application

引言

图形设备接口 (GDI) 是一个用于在 Windows 中绘图的设备无关库。它可以用于在屏幕、打印机、传真等上输出。GDI 是一个能完成工作的得力助手。尽管 GDI 库包含许多简单的绘图例程,但当您需要输出到打印机以外的设备时,它会变得更加复杂。在这种情况下,您可能需要处理缩放和反转的坐标系统。本文将引导您了解三种不同的映射模式。我们还将学习如何将 GDI 操作保存到增强型图元文件中,并重新播放它们。

GDI 中的简单绘图

首先,GDI 在 DevicesContext(设备上下文)上进行绘图。这是一种内存中的虚拟区域。下面是如何绘制一个矩形。假设我们需要响应一个 paint(绘制)事件并重绘窗口。

case WM_PAINT:
{
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hwnd, &ps);

    // Drawing code begin
    //    
    RECT rc;
    rc.top = 100;
    rc.left = 100;
    rc.bottom = 300;
    rc.right = 300;

    HBRUSH greenBrush=CreateSolidBrush(RGB(0,255,0));
    FillRect(hdc, &rc, greenBrush);
    DeleteObject(greenBrush);
    //
    // Drawing code end

    EndPaint(hwnd, &ps);
}

此代码将在屏幕上绘制一个 200x200 的填充绿色矩形,其左上角位于 (100,100) 位置。

输出到打印机

如果我们尝试在打印机上进行输出会怎样?

void GDIDraw::Print(HWND hWnd)
{
   PRINTDLG pd = {0};
   pd.lStructSize = sizeof( pd );
   pd.hwndOwner = hWnd;
   pd.Flags = PD_RETURNDC;
   
   // Retrieves the printer DC
   if (PrintDlg(&pd))
   {
      HDC hdc = pd.hDC;
      StartDoc (hdc, &di);
      StartPage (hdc);    
  
      // Drawing code begin
      //    
      RECT rc;
      rc.top = 100;
      rc.left = 100;
      rc.bottom = 300;
      rc.right = 300;

      HBRUSH greenBrush=CreateSolidBrush(RGB(0,255,0));
      FillRect(hdc, &rc, greenBrush);
      DeleteObject(greenBrush);
      //
      // Drawing code end

      EndPage (hdc);
      EndDoc(hdc);
      DeleteObject(hdc);
   }
}

Screenshot of demo application

这样做会显示一个非常小的矩形,但它会非常小,几乎看不见。

图像如此之小的原因是,与屏幕相比,纸张的分辨率非常高。打印机在打印我们的矩形时可能会使用 600 dpi,使其在纸上占用 1/3 英寸的空间。换句话说,我们必须放大矩形,使其在纸上更大。但这时我们还需要考虑尺寸问题。100 像素是多少?这取决于屏幕和打印机的 dpi。通常,屏幕输出为 72 dpi。100 像素在屏幕上约为 100 像素 / 72 dpi = 1.39 英寸。要使其在打印机上具有相同的大小,您必须乘以打印机的 dpi。1.39 英寸 x 600 dpi = 833 设备单位。这意味着缩放因子为 600/72 = 8.33。GetDeviceCaps(hdc, LOGPIXELSX)GetDeviceCaps(hdc, LOGPIXELSY) 返回我的 XPS 打印机的打印 DPI 600。

double scaleX = GetDeviceCaps(hdc, LOGPIXELSY) / 72;
double scaleY = GetDeviceCaps(hdc, LOGPIXELSX) / 72;

// Scale for printing
rc.top *= scaleY;
rc.bottom *= scaleY;
rc.left *= scaleX;
rc.right = *= scaleX;

Output scaled to fill the page

窗口对于打印机来说太大了

如果窗口的尺寸(以英寸为单位)大于您打印的纸张,会怎样?在这种情况下,您仍然必须放大矩形,但缩放必须在纸张的范围内。

我们考虑 A4 纸张尺寸,在纵向布局下宽度为 210 毫米,屏幕分辨率为 1200x800 像素。1200 像素 / 72 dpi / 25.4mm = 423 毫米宽。换句话说,我的屏幕宽度是 A4 纸的两倍。如果我们想填充宽度,我们就必须进行反向计算。屏幕上的最大宽度仍然是 1200,但在打印机上,它将是 210mm / 25.4mm/inch * 600 dpi = 4961 设备单位。我们如何将 1200 像素映射到 4961 设备单位?答案是使用 *映射模式*。

映射模式

逻辑视图和设备视图之间的转换由 GDI 映射模式处理。

  • MM_TEXT
  • MM_ISOTROPIC
  • MM_HIMETRIC

还有其他模式,但本文只介绍这三种。您可以通过调用 SetMapMode(HDC hdc, int mode) 来设置映射模式。

MM_TEXT

MM_TEXT 将 1 个逻辑像素映射到 1 个设备像素。对于屏幕来说,这是最合适的映射,因为在这种情况下不需要缩放。这是默认模式。

MM_HIMETRIC

MM_HIMETRIC 进行反向映射,将 1 个设备像素映射到 1 个逻辑像素。1 个设备像素对应纸张上的 0.01 毫米。请注意,逻辑视图不再对应屏幕,即使您想要打印屏幕。无论是尺寸还是形状。

A4 纸张宽度为 210 毫米。逻辑单位数为 210 毫米 / 0.01 毫米/单位 = 21000 单位。Y 轴是*负值*,测量值为 297 毫米 = -29700 单位。假设您的窗口尺寸为 1200x800。X 轴的缩放因子为 21000 单位 / 1200。Y 轴的缩放因子相同但为负。 (有些打印机在 X 和 Y 轴上输出不同的 dpi,在这种情况下,还必须补偿该比例。)

double scaleX = 21000.0/1200.0
double scaleY = -scaleX;

RECT rc;
rc.top = 100;
rc.left = 100;
rc.bottom = 300;
rc.right = 300;

// Scale for printing
rc.top *= scaleY;
rc.bottom *= scaleY;
rc.left *= scaleX;
rc.right = *= scaleX;

MM_ISOTROPIC

MM_ISOTROPIC 有点像 MM_TEXT HIMETRIC 的混合。此模式支持逻辑视图和设备视图之间的自动缩放。这两个视图不必具有相同的分辨率,但应该具有相同的比例。否则,缩放将失真。

SetWindowExt(hdc, 120, 80);
SetViewPortExt(hdc, 1200, 800);

如果您绘制一条 120 个逻辑单位宽的线,它将填充 1200 个设备单位。如果在打印机上输出,则有一个陷阱。xy 的比例是固定的。它取决于纸张尺寸以及是纵向还是横向打印。因此,为了使缩放正常工作,我们必须扩展逻辑视图,使其与我们输出到的纸张具有相同的比例。对于在纵向布局的 A4 纸上打印的 1200 x 800 屏幕,我们可以进行以下计算来获取虚拟区域的大小。

int printerXUnits = GetDeviceCaps (hdc, HORZRES);
int printerYUnits = GetDeviceCaps (hdc, VERTRES);
double windowX = 1200.0;
double windowY = 800.0;
double scaleX = printerXUnits/x;
double scaleY = printerYUnits/y;
double Y = printerYUnits/printerXUnits*windowX = 1697.14

为了在纵向布局中获得纸张的比例大小,我们的逻辑视图应测量 (1200 x 1697)。

SetWindowExt(hdc, 1200, 1697);
SetViewPortExt(hdc, 1200, -1697);

现在窗口的宽度将占据整个 X 轴。缩放是自动的。

RECT rc;
rc.top = 100;
rc.left = 100;
rc.bottom = 300;
rc.right = 300;

保存图元文件

GDI 几乎就像矢量图形。我们可以保存 GDI 操作而不是保存图像。我们可以重新加载图元文件,并在不同的分辨率下重放它。

以适合打印机的分辨率保存它是有意义的。A4 格式的尺寸为 210x297 毫米。这相当于 21000x29700 单位。获取打印机 Letter 或 A4 尺寸的一种方法是打开 XPS 打印机,并从驱动程序读取尺寸。

HDC printerDC = CreateDC(TEXT("Microsoft XPS Document Writer"),
              TEXT("Microsoft XPS Document Writer"),NULL, NULL);
int paperWidthInMillimeter = GetDeviceCaps(printerDC, HORZSIZE);
int paperHeightInMillimeter = GetDeviceCaps(printerDC, VERTSIZE);
int paperWidth = paperWidthInMillimeter * 100;
int paperHeight = paperHeightInMillimeter * 100; 
RECT rect;
rect.top = 0;
rect.left = 0;
rect.bottom = paperHeight;
rect.right = paperWidth;

之后,我们可以创建一个与图元文件关联的新设备上下文。

HDC metaHdc = CreateEnhMetaFile(printerDC, filename.c_str(), &rect, "GDIApp");
if (metaHdc)
  {
    gdiDraw->Paint(metaHdc, hWnd, TRUE);
    CloseEnhMetaFile(metaHdc);
}
DeleteDC(printerDC);

加载和重放图元文件

重放图元文件几乎和保存它一样简单。

在屏幕上显示图元文件的一种简单方法是先将其重放到具有所需尺寸的位图上。如果图元文件的大小比屏幕大或小,如果您需要调整其大小,这会非常方便。首先要做的是找出图元文件的尺寸。

HENHMETAFILE m_metafile = GetEnhMetaFile(filename);

BOOL GetFrameRectFromMetafile(HENHMETAFILE hemf, RECT* rect)
{
   ENHMETAHEADER emh = { 0 };
   emh.nSize = sizeof(ENHMETAHEADER);
   if( GetEnhMetaFileHeader( hemf,  sizeof( ENHMETAHEADER ), &emh ) == 0)
      return FALSE;

   if (rect == NULL)
      return FALSE;

   rect->top = emh.rclFrame.top;
   rect->bottom = emh.rclFrame.bottom;
   rect->left = emh.rclFrame.left;
   rect->right = emh.rclFrame.right;
   return TRUE;
}

如果我们以 A4 尺寸将图元文件保存为打印文档,文档的整个高度将无法适应屏幕,至少在我们将文档设为占据屏幕整个宽度的情况下是这样。在这种情况下,我们需要创建一个高度适合文档总尺寸的位图。我们知道所需的宽度,即屏幕宽度。现在我们需要计算高度。

RECT metaFrameRect = {0};
GetFrameRectFromMetafile(m_metafile, &metaFrameRect);
long lWidth = (long)(abs(metaFrameRect.left - metaFrameRect.right));
long lHeight =(long)(abs(metaFrameRect.top - metaFrameRect.bottom));
int adjustedX = windowX;
int adjustedY = lHeight * windowX / lWidth;

现在,我们需要将文件重放到位图中。

RECT adjustedRect = {0};
adjustedRect.right = adjustedX - 1;
adjustedRect.bottom = adjustedY - 1;

// Paint the metafile into a bitmap
HDC memDC = ::CreateCompatibleDC(hdc);
HBITMAP bitmap = ::CreateCompatibleBitmap(hdc, adjustedX, adjustedY);
::SelectObject(memDC,bitmap);
   
// The metafile may lack background color
// Set it, otherwise the background color will be black
HBRUSH whiteBrush=CreateSolidBrush(RGB(255,255,255));
FillRect(memDC, &adjustedRect, whiteBrush);
DeleteObject(whiteBrush);

// Play metafile
PlayEnhMetaFile(memDC,m_metafile,&adjustedRect);    
DWORD dwRet = GetLastError();

最后一步是将其复制到屏幕上。

// Now we can copy the bitmap to screen
// AdjustedY can be bigger than the screen
BitBlt(hdc,0, 0,adjustedX, min(windowY, adjustedY), memDC, 0, 0, SRCCOPY);
DeleteObject(bitmap);
DeleteDC(memDC);

演示应用程序

我制作了一个演示应用程序,该应用程序输出一个矩形和一些文本。单击按钮可在 MM_TEXTMM_HIMETRICMM_ISOTROPIC 三种映射模式之间循环。

总而言之,程序有 6 条不同的路径,每种映射模式 2 条。在输出到打印机时,我们需要进行更多的计算来正确处理缩放。字体也在 DemoApp 中进行缩放,这可能比缩放线条和方框要棘手一些。

Screenshot of demo application

单击按钮可在映射模式之间切换,并使用“文件”菜单中的“打印”选项打印页面(输出到 XPS 打印机以节省墨水和纸张)。

加载图元文件时的背景颜色不是白色。如果未显式设置,图元文件实际上缺少背景颜色。我更改了颜色,以便更容易看到与正常绘制时的区别。如果这是您想要的颜色,请自己将其更改为白色。

代码

我使用了 Win32 GDI 函数,其中第一个参数是 hdc。另一种选择是使用 MFC 类,但我使用的是家里的 VS2012 Express 版本,那里没有 MFC。但 MFC 类只是 GDI 函数的包装器,因此相同的原理和规则适用于这些类。

我刚刚测试了代码和使用毫米和映射模式 MM_HIMETRIC 的转换。还有一个名为 MM_HIENGLISH 的映射模式,它使用英寸。但考虑到 MM_HIMETRIC 的代码,使其正常工作应该不会太费力。

关注点

LPtoDP DPtoLP 这样的函数用于在逻辑坐标和设备坐标之间转换值,它们返回 long 数据类型。这些是四舍五入的值。考虑将 100 个单位或更多转换为获取更多小数。之后,使用浮点算术进行所有缩放。

GDI 比我最初想象的要复杂得多。事物看起来失真、尺寸不正确,甚至根本不出现。在我寻求关于 GDI 的答案的过程中,我在互联网上找到了各种来源。但我建议阅读一本好的 GDI 书籍。Charles Petzold(我最喜欢的作者之一)在 1998 年写了一本《Programming Windows》(第 5 版)。这是一本旧书,但 GDI 也是一项旧技术。

历史

  • 2014 年 4 月 24 日 - V1.0 第一个版本
  • 2014 年 4 月 27 日 - V1.1 添加了对增强型图元文件的支持
© . All rights reserved.