1. 项目背景与需求分析
CR80工作证(85.6mm×54mm)是国际标准尺寸的证件卡,广泛应用于企业工牌、校园卡等场景。作为有十几年证卡打印机开发经验的工程师,我经常遇到客户需要将现有系统集成工作证打印功能的需求。传统做法是依赖打印机厂商提供的专用软件,但这会带来额外成本和使用限制。
使用VC++直接控制打印机可以实现:
- 完全自主的打印流程控制
- 与现有管理系统无缝集成
- 避免第三方软件的授权费用
- 灵活定制打印内容和样式
注意:虽然本文以CR80为例,但所述方法同样适用于其他尺寸的卡片打印,只需调整相应参数即可。
2. 开发环境准备
2.1 基础开发环境配置
首先需要确保开发环境正确配置:
- 安装Visual Studio(建议2015或以上版本)
- 创建MFC应用程序项目(对话框或单文档架构均可)
- 配置项目属性为使用多字节字符集(传统打印API多使用ANSI编码)
2.2 打印相关头文件引入
在stdafx.h或项目主头文件中添加以下Windows打印API头文件:
cpp复制#include <windows.h> // Windows基础API
#include <winspool.h> // 打印后台处理API
#include <commdlg.h> // 通用对话框API(用于打印对话框)
#include <gdiplus.h> // GDI+绘图接口(高级图形处理)
#pragma comment(lib, "gdiplus.lib") // 链接GDI+库
提示:如果项目已使用MFC,可以省略部分基础头文件,因为MFC已封装了这些功能。
2.3 GDI+初始化
在应用程序初始化时(如CWinApp::InitInstance中)添加GDI+初始化代码:
cpp复制Gdiplus::GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
并在程序退出时清理:
cpp复制Gdiplus::GdiplusShutdown(gdiplusToken);
3. 打印机控制核心实现
3.1 打印机选择与初始化
创建打印机选择对话框并获取设备上下文:
cpp复制CPrintDialog printDlg(FALSE);
if (printDlg.DoModal() == IDOK)
{
HDC hdcPrint = printDlg.CreatePrinterDC();
if (hdcPrint == NULL)
{
AfxMessageBox(_T("无法创建打印机DC!"));
return;
}
// 获取打印机属性
DEVMODE* pDevMode = printDlg.GetDevMode();
// ...后续打印逻辑
}
3.2 毫米到像素的精确转换
CR80卡片的精确尺寸是85.6mm×54mm,需要根据打印机DPI进行精确换算:
cpp复制int nHorzRes = GetDeviceCaps(hdcPrint, HORZRES); // 水平分辨率(像素)
int nVertRes = GetDeviceCaps(hdcPrint, VERTRES); // 垂直分辨率(像素)
int nLogPixelsX = GetDeviceCaps(hdcPrint, LOGPIXELSX); // 水平DPI
int nLogPixelsY = GetDeviceCaps(hdcPrint, LOGPIXELSY); // 垂直DPI
// 毫米转像素函数
int MMToPixelX(int mm, HDC hdc)
{
return (int)((mm * GetDeviceCaps(hdc, LOGPIXELSX)) / 25.4);
}
int MMToPixelY(int mm, HDC hdc)
{
return (int)((mm * GetDeviceCaps(hdc, LOGPIXELSY)) / 25.4);
}
3.3 打印页面设置
设置打印页面方向和边距:
cpp复制// 设置为横向打印(根据打印机实际进纸方向调整)
pDevMode->dmOrientation = DMORIENT_LANDSCAPE;
pDevMode->dmFields |= DM_ORIENTATION;
// 设置无边距打印(如果打印机支持)
pDevMode->dmFields |= DM_PAPERSIZE;
pDevMode->dmPaperSize = DMPAPER_USER;
pDevMode->dmPaperWidth = MMToPixelX(85.6, hdcPrint);
pDevMode->dmPaperLength = MMToPixelY(54, hdcPrint);
// 应用打印机设置
ResetDC(hdcPrint, pDevMode);
4. 工作证内容绘制实现
4.1 基础绘制框架
创建打印类封装核心功能:
cpp复制class CCardPrinter {
public:
BOOL PrintCard(CDC* pDC, LPCTSTR lpszName, LPCTSTR lpszDept,
LPCTSTR lpszID, CBitmap* pPhoto = NULL);
private:
void DrawBorder(CDC* pDC);
void DrawTextInfo(CDC* pDC, LPCTSTR lpszName, LPCTSTR lpszDept, LPCTSTR lpszID);
void DrawPhoto(CDC* pDC, CBitmap* pPhoto);
};
4.2 绘制证件边框
cpp复制void CCardPrinter::DrawBorder(CDC* pDC)
{
CPen pen(PS_SOLID, 2, RGB(0,0,0)); // 2像素宽黑色边框
CPen* pOldPen = pDC->SelectObject(&pen);
// 外边框(留1mm边距)
int left = MMToPixelX(1, pDC->GetSafeHdc());
int top = MMToPixelY(1, pDC->GetSafeHdc());
int right = MMToPixelX(84.6, pDC->GetSafeHdc());
int bottom = MMToPixelY(53, pDC->GetSafeHdc());
pDC->Rectangle(left, top, right, bottom);
// 内部分隔线等绘制逻辑...
pDC->SelectObject(pOldPen);
}
4.3 绘制文字信息
cpp复制void CCardPrinter::DrawTextInfo(CDC* pDC, LPCTSTR lpszName,
LPCTSTR lpszDept, LPCTSTR lpszID)
{
// 设置高质量抗锯齿文本
pDC->SetBkMode(TRANSPARENT);
pDC->SetTextColor(RGB(0,0,0));
// 姓名(居中显示)
CFont fontName;
fontName.CreatePointFont(220, _T("黑体")); // 22磅
CFont* pOldFont = pDC->SelectObject(&fontName);
CRect rectName(MMToPixelX(10,pDC->GetSafeHdc()), MMToPixelY(10,pDC->GetSafeHdc()),
MMToPixelX(75,pDC->GetSafeHdc()), MMToPixelY(25,pDC->GetSafeHdc()));
pDC->DrawText(lpszName, rectName, DT_CENTER|DT_VCENTER|DT_SINGLELINE);
// 部门/ID等其他信息...
pDC->SelectObject(pOldFont);
}
4.4 绘制照片
cpp复制void CCardPrinter::DrawPhoto(CDC* pDC, CBitmap* pPhoto)
{
if (pPhoto == NULL) return;
BITMAP bm;
pPhoto->GetBitmap(&bm);
// 照片区域(右侧30mm宽度)
CRect rectPhoto(MMToPixelX(55,pDC->GetSafeHdc()), MMToPixelY(10,pDC->GetSafeHdc()),
MMToPixelX(82,pDC->GetSafeHdc()), MMToPixelY(40,pDC->GetSafeHdc()));
// 保持宽高比缩放
double dScale = min((double)rectPhoto.Width()/bm.bmWidth,
(double)rectPhoto.Height()/bm.bmHeight);
int nWidth = (int)(bm.bmWidth * dScale);
int nHeight = (int)(bm.bmHeight * dScale);
CDC memDC;
memDC.CreateCompatibleDC(pDC);
CBitmap* pOldBmp = memDC.SelectObject(pPhoto);
pDC->SetStretchBltMode(COLORONCOLOR);
pDC->StretchBlt(rectPhoto.left + (rectPhoto.Width()-nWidth)/2,
rectPhoto.top + (rectPhoto.Height()-nHeight)/2,
nWidth, nHeight, &memDC, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY);
memDC.SelectObject(pOldBmp);
}
5. 完整打印流程实现
5.1 打印任务启动
cpp复制void CMyApp::OnPrintCard()
{
CPrintDialog printDlg(FALSE);
if (printDlg.DoModal() == IDOK)
{
HDC hdcPrint = printDlg.CreatePrinterDC();
if (hdcPrint)
{
CDC dcPrint;
dcPrint.Attach(hdcPrint);
DOCINFO di = {0};
di.cbSize = sizeof(DOCINFO);
di.lpszDocName = _T("工作证打印");
if (dcPrint.StartDoc(&di) > 0)
{
if (dcPrint.StartPage() > 0)
{
CCardPrinter printer;
printer.PrintCard(&dcPrint, _T("张三"), _T("技术部"),
_T("EMP20230001"), &m_bmpPhoto);
dcPrint.EndPage();
}
dcPrint.EndDoc();
}
dcPrint.Detach();
DeleteDC(hdcPrint);
}
}
}
5.2 多张卡片批量打印
cpp复制void PrintMultipleCards(CDC* pDC, const CArray<CCardInfo>& cards)
{
DOCINFO di = {0};
di.cbSize = sizeof(DOCINFO);
di.lpszDocName = _T("批量打印工作证");
if (pDC->StartDoc(&di) > 0)
{
CCardPrinter printer;
for (int i = 0; i < cards.GetSize(); i++)
{
if (pDC->StartPage() > 0)
{
const CCardInfo& card = cards[i];
printer.PrintCard(pDC, card.m_strName, card.m_strDept,
card.m_strID, &card.m_bmpPhoto);
pDC->EndPage();
}
}
pDC->EndDoc();
}
}
6. 常见问题与解决方案
6.1 打印位置偏移问题
现象:打印内容与预期位置有偏移
原因分析:
- 打印机物理边距导致
- DPI计算不准确
- 页面方向设置错误
解决方案:
- 在打印机属性中确认实际可打印区域
- 添加校准功能,打印测试网格进行微调
- 使用以下代码获取实际可打印区域:
cpp复制int nPhysOffsetX = GetDeviceCaps(hdcPrint, PHYSICALOFFSETX);
int nPhysOffsetY = GetDeviceCaps(hdcPrint, PHYSICALOFFSETY);
int nPhysWidth = GetDeviceCaps(hdcPrint, PHYSICALWIDTH);
int nPhysHeight = GetDeviceCaps(hdcPrint, PHYSICALHEIGHT);
6.2 照片打印质量差
现象:照片模糊或有色差
优化方案:
- 使用GDI+进行高质量图像处理:
cpp复制Gdiplus::Bitmap* pBitmap = Gdiplus::Bitmap::FromHBITMAP(
card.m_bmpPhoto, NULL);
Gdiplus::Graphics graphics(pDC->GetSafeHdc());
graphics.SetInterpolationMode(
Gdiplus::InterpolationModeHighQualityBicubic);
graphics.DrawImage(pBitmap, rectPhoto.left, rectPhoto.top,
rectPhoto.Width(), rectPhoto.Height());
- 确保原始照片分辨率足够(建议300dpi以上)
- 在打印前对照片进行锐化处理
6.3 打印机内存不足
现象:复杂卡片打印时打印机报内存错误
优化策略:
- 分块发送打印数据
- 降低图像分辨率(但保持最小300dpi)
- 使用光栅图形而非矢量图形
- 增加打印机内存或更换更高配置打印机
7. 高级功能扩展
7.1 条形码/二维码集成
cpp复制void CCardPrinter::DrawBarcode(CDC* pDC, LPCTSTR lpszCode)
{
// 使用第三方库如Zint生成条形码位图
// 或调用打印机内置条码打印命令(ESC/POS等)
// 示例:Code128条形码
CRect rectBarcode(MMToPixelX(10,pDC->GetSafeHdc()),
MMToPixelY(45,pDC->GetSafeHdc()),
MMToPixelX(80,pDC->GetSafeHdc()),
MMToPixelY(50,pDC->GetSafeHdc()));
// 实际项目中应集成专业条码生成库
// 这里简化演示
pDC->Rectangle(rectBarcode);
pDC->DrawText(lpszCode, rectBarcode, DT_CENTER|DT_VCENTER);
}
7.2 磁条/芯片数据写入
cpp复制void WriteMagstripeData(LPCTSTR lpszTrack1, LPCTSTR lpszTrack2, LPCTSTR lpszTrack3)
{
// 需要打印机支持磁条编码功能
// 通常通过发送特殊ESC/POS命令实现
// 示例代码(实际命令因打印机型号而异):
CString strCmd;
strCmd.Format(_T("\x1B;m%s\x0D\x1B;n%s\x0D\x1B;o%s\x0D"),
lpszTrack1, lpszTrack2, lpszTrack3);
// 发送到打印机
DWORD dwWritten;
HANDLE hPrinter = GetPrinterHandle();
WritePrinter(hPrinter, (LPVOID)(LPCTSTR)strCmd,
strCmd.GetLength()*sizeof(TCHAR), &dwWritten);
}
7.3 双面打印实现
cpp复制void PrintTwoSidedCard(CDC* pDC, CCardInfo& front, CCardInfo& back)
{
// 设置双面打印模式
DEVMODE* pDevMode = /* 获取DEVMODE */;
pDevMode->dmDuplex = DMDUP_VERTICAL;
pDevMode->dmFields |= DM_DUPLEX;
ResetDC(pDC->GetSafeHdc(), pDevMode);
// 打印正面
if (pDC->StartPage() > 0)
{
PrintFrontSide(pDC, front);
pDC->EndPage();
}
// 打印背面
if (pDC->StartPage() > 0)
{
PrintBackSide(pDC, back);
pDC->EndPage();
}
}
在实际项目中,我通常会创建一个完整的证件打印框架,将上述功能模块化,便于不同项目复用。根据具体需求,还可以添加以下高级功能:
- 数据库连接,直接从人员管理系统获取打印数据
- 打印模板设计器,允许用户自定义布局
- 打印队列管理,支持大批量连续打印
- 打印预览功能,减少试错成本
对于刚接触证卡打印开发的工程师,建议先从单面打印基础功能开始,逐步添加复杂功能。每次添加新功能时,都要进行充分的打印机兼容性测试,因为不同品牌、型号的打印机对特殊命令的支持可能有差异。