生而自由

自由而无用的灵魂

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的头文件。

#include <GdiPlus.h>
using namespace Gdiplus;

#include "Winspool.h"

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

//用于描述屏幕与打印机的分辨率等信息
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+的启动与关闭

// Gdi配置项
GdiplusStartupInput m_gdiplusStartupInput;
ULONG_PTR m_gdiplusToken;

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

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

辅助方法

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;
}

测试方法

// 显示默认打印机的分辨率等信息
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);
}

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注