0%

Windows下的打印Demo

前言

一晃过去快5年了,打印的解决方案是工作后的第一个主要内容,从此也开始了一半后端一半客户端的工作经历。因为种种原因,还没有在这里总结过相关的技术,现在稍微记录一下。

打印的基础概念

Windows下对于打印有相当良好的封装,任何打印机只要有符合标准的驱动,应用程序可以用Windows提供的一套统一的API来实现打印功能。相关的API主要在Winspool.h下,如OpenPrinter、StartDoc等,本Demo使用了MFC中的CDC封装,本质上与Winspool下的API没有区别。

打印时有一点需要注意的是分辨率的映射,一般来说遵循“所见即所得”的规则。对于Windows来说,显示器、打印机都是DC(Device Context),但它们有着不同的分辨率和PPI(Pixels Per Inch,像素密度),那么在打印时,要做到内容在显示器上显示为1厘米长,那么打印在纸张上是也要是1厘米长,即物理尺寸保持一致 。当然屏幕显示还有内容缩放等更复杂的场景,这里先不予以考虑。

因此虽然一般的激光打印机的分辨率比显示器高,但由于它的PPI也更高,所以在保持打印内容物理尺寸一致的情况下,它所能展示的内容其实比屏幕要少。换一种角度想,这其实就是因为纸张的物理尺寸一般小于显示器的物理尺寸...

Demo概述

该Demo是基于MFC的,直接使用了MFC中的CDC、CPrintDialog等组件,然后使用Gdi+(GdiPlus)进行绘制。在绘制之前,先计算了打印机相对显示器的逻辑分辨率,并设置了Gdi+的映射模式。

头文件与结构体

需要添加GdiPlus和Winspool的头文件。

1
2
3
4
#include <GdiPlus.h>
using namespace Gdiplus;

#include "Winspool.h"

定义了逻辑分辨率相关的结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//用于描述屏幕与打印机的分辨率等信息
typedef struct tagPrintDeviceInfo
{
int ScreenPixelX; // 屏幕宽度,单位像素
int ScreenPixelY; // 屏幕高度,单位像素
int ScreenPPIX; // 屏幕PPI,横向
int ScreenPPIY; // 屏幕PPI,竖向
int PrinterPixelX; // 打印机宽度,单位像素
int PrinterPixelY; // 打印机高度,单位像素
int PrinterPPIX; // 打印机PPI,横向
int PrinterPPIY; // 打印机PPI,竖向
int PrintRatePixelX; // 打印机虚拟(逻辑)分辨率,横向
int PrintRatePixelY; // 打印机虚拟(逻辑)分辨率,竖向
} PrintDeviceInfo;

Gdi+的启动与关闭

1
2
3
4
5
6
7
8
9
// Gdi配置项
GdiplusStartupInput m_gdiplusStartupInput;
ULONG_PTR m_gdiplusToken;

// 应用启动时启动Gdi+
Status gdiResult = GdiplusStartup(&m_gdiplusToken, &m_gdiplusStartupInput, NULL);

// 应用退出时关闭Gdi+
GdiplusShutdown(m_gdiplusToken);

辅助方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
PrintDeviceInfo CPrintDemoDlg::GetDeviceInfo(CDC &printerDC)
{
PrintDeviceInfo info;

// 获取屏幕信息
HDC screenHDC = ::GetDC(NULL);
CDC* screenDC = CDC::FromHandle(screenHDC);

// 获取屏幕的像素数(分辨率)
info.ScreenPixelX = screenDC->GetDeviceCaps(HORZRES);
info.ScreenPixelY = screenDC->GetDeviceCaps(VERTRES);

// 获取屏幕的PPI
info.ScreenPPIX = screenDC->GetDeviceCaps(LOGPIXELSX);
info.ScreenPPIY = screenDC->GetDeviceCaps(LOGPIXELSY);

DeleteDC(screenHDC);

// 获取打印机的像素数(分辨率)
info.PrinterPixelX = printerDC.GetDeviceCaps(HORZRES);
info.PrinterPixelY = printerDC.GetDeviceCaps(VERTRES);

// 获取打印机的PPI
info.PrinterPPIX = printerDC.GetDeviceCaps(LOGPIXELSX);
info.PrinterPPIY = printerDC.GetDeviceCaps(LOGPIXELSY);

// 计算PPI比例
double ratePPIX = info.PrinterPPIX;
ratePPIX = ratePPIX / info.ScreenPPIX;

double ratePPIY = info.PrinterPPIY;
ratePPIY = ratePPIY / info.ScreenPPIY;

// 计算出打印机实际支持的屏幕对照像素数
// 打印时一般遵循物理尺寸一致的“所见即所得”模式
// 因为打印机的PPI一般较高,因此需要用几个像素来对应屏幕的一个像素,换算下来之后,打印机的对照分辨率也就降低了
info.PrintRatePixelX = static_cast<int>(info.PrinterPixelX / ratePPIX);
info.PrintRatePixelY = static_cast<int>(info.PrinterPixelY / ratePPIY);

return info;
}

// 获取默认打印机DC
void CPrintDemoDlg::GetDefaultPrinterCtx(CDC &printerDC, LPDEVMODE &printerDevMode)
{
// 设置打印对话框风格
DWORD dwflags = PD_PAGENUMS | PD_HIDEPRINTTOFILE | PD_RETURNDEFAULT;
// 创建打印对话框
CPrintDialog printDlg(false, dwflags, NULL);

// 获取设备默认项
if (!printDlg.GetDefaults())
{
return;
}

if (printerDC.Attach(printDlg.GetPrinterDC()))
{
//获取DEVMODE
printerDevMode = printDlg.GetDevMode();
}
}

// 获取指定打印机的DEVMODE
bool CPrintDemoDlg::GetTargetPrinterDevMode(CString strPrintName, LPDEVMODE &printerDevMode)
{
HANDLE hPrinter;
if (!OpenPrinter(strPrintName.GetBuffer(0), &hPrinter, NULL))
return false;

DWORD dwBytesNeeded;
dwBytesNeeded = DocumentProperties(NULL, hPrinter, NULL, NULL, NULL, 0);

HGLOBAL lhDevMode = GlobalAlloc(GHND, dwBytesNeeded);
printerDevMode = (LPDEVMODE)GlobalLock(lhDevMode);

DWORD dwRet = DocumentProperties(NULL, hPrinter, NULL, printerDevMode, NULL, DM_OUT_BUFFER);
ASSERT(IDOK == dwRet);

ClosePrinter(hPrinter);
return true;
}

// 获取指定(名称)打印机的DC
bool CPrintDemoDlg::GetTargetPrinterCtx(CString strPrintName, CDC &printerDC, LPDEVMODE &printerDevMode)
{
// 这里先取指定打印机的DEVMODE
if (!GetTargetPrinterDevMode(strPrintName, printerDevMode))
{
return false;
}

// 设置纸张, nPaper 是打印机支持的纸张列表的序号
// 使用 DeviceCapabilities( strPrinterName, NULL, DC_PAPERNAMES, NULL, NULL ) 来获取打印机支持的所有纸张
// printerDevMode->dmPaperSize = nPaper;

// 设置纸张方向
// 使用 DeviceCapabilities( strPrinterName, NULL, DC_ORIENTATION, NULL, NULL ) 来判断打印机是否支持横向
// printerDevMode->dmOrientation = nOrientation

// 设置单双面
// 使用 DeviceCapabilities( strPrinterName, NULL, DC_DUPLEX, NULL, NULL ) 来判断打印机是否支持双面打印
// printerDevMode->dmDuplex = nDuplex;

//创建打印机DC
ASSERT(printerDC.CreateDC(NULL, strPrintName, NULL, printerDevMode));

return true;
}

// 打印图片至DC
bool CPrintDemoDlg::PrintImage(CDC &printerDC, Image &image, int xSrc, int ySrc, int nSrcWidth, int nSrcHeight, int xDest, int yDest, int nDestWidth, int nDestHeight)
{
RectF destRC;
destRC.X = xDest;
destRC.Y = yDest;
destRC.Width = nDestWidth;
destRC.Height = nDestHeight;

// 修改映射模式
PrintDeviceInfo deviceInfo = GetDeviceInfo(printerDC);

printerDC.SetMapMode(MM_ANISOTROPIC);
printerDC.SetWindowExt(deviceInfo.PrintRatePixelX, deviceInfo.PrintRatePixelY); /* 逻辑分辨率 */
printerDC.SetViewportExt(deviceInfo.PrinterPixelX, deviceInfo.PrinterPixelY); /* 物理分辨率 */

Graphics graphics(printerDC.m_hDC);
graphics.SetPageUnit(Gdiplus::Unit::UnitPixel);

ASSERT(Gdiplus::Ok == graphics.GetLastStatus());

Status gdiResult = graphics.DrawImage(&image, destRC, xSrc, ySrc, nSrcWidth, nSrcHeight, UnitPixel);

ASSERT(Gdiplus::Ok == gdiResult);

return true;
}

测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 显示默认打印机的分辨率等信息
void CPrintDemoDlg::OnBnClickedBtnTest1()
{
// 设置打印对话框风格
DWORD dwflags = PD_PAGENUMS | PD_HIDEPRINTTOFILE | PD_RETURNDEFAULT;
// 创建打印对话框
CPrintDialog printDlg(false, dwflags, NULL);

// 获取设备默认项
if (!printDlg.GetDefaults())
{
MessageBox(_T("获取默认打印机失败!"));
return;
}

CDC tempPrintDC;
if (tempPrintDC.Attach(printDlg.GetPrinterDC()))
{
PrintDeviceInfo info = GetDeviceInfo(tempPrintDC);

CString strInfo;
strInfo.Format(_T("打印机分辨率:%d,%d; 打印机PPI:%d; 屏幕分辨率:%d,%d; 屏幕PPI: %d; 打印机实际对照分辨率:%d,%d"), info.PrinterPixelX, info.PrinterPixelY, info.PrinterPPIX,
info.ScreenPixelX, info.ScreenPixelY, info.ScreenPPIX, info.PrintRatePixelX, info.PrintRatePixelY);
MessageBox(strInfo);
}
else
{
MessageBox(_T("获取打印机上下文失败!"));
return;
}
}

// 打印一张图片
void CPrintDemoDlg::OnBnClickedBtnTest2()
{
CDC m_printerDC; /* 打印机上下文 */
LPDEVMODE m_pPrintDevMode; /* 打印机DEVMODE */

// 获取默认打印机
GetDefaultPrinterCtx(m_printerDC, m_pPrintDevMode);

// 获取指定打印机
// GetTargetPrinterCtx(_T("Microsoft XPS Document Writer"), m_printerDC, m_pPrintDevMode);

// 开始打印
int m_nPrintJobId = m_printerDC.StartDoc(_T("Work01"));

// 打印图片,1.png的分辨率为200x200
Image img(_T("D:\\\1.png"), FALSE);
PrintImage(m_printerDC, img, 0, 0, 200, 200, 0, 0, 200, 200);

// 结束打印
int result = m_printerDC.EndDoc();

// LPDEVMODE 需要释放
GlobalUnlock(m_pPrintDevMode);
GlobalFree(m_pPrintDevMode);
}