1. 项目背景与需求解析
在仓储物流、零售门店和医疗设备管理等场景中,便携式蓝牙标签打印机已经成为标配设备。这类设备通常通过Android移动终端进行控制,而Qt作为跨平台开发框架,能够帮助开发者用C++代码快速实现Android端的蓝牙打印功能。这个方案完美解决了传统Java开发中存在的性能瓶颈和跨平台复用难题。
去年我在为一家连锁药店开发药品管理系统时,就遇到了这样的需求:药剂师需要用手持PDA扫描药品条形码后,立即打印包含药品名称、批号和有效期的小标签。经过技术选型,最终采用Qt+C++的方案实现了这个功能,打印响应时间从Java版的2.3秒缩短到0.8秒。
2. 技术架构设计
2.1 核心组件选型
整个系统由三个关键模块构成:
- Qt蓝牙模块:负责设备发现、连接管理
- QZXing库:用于生成条形码/二维码
- ESC/POS指令集:控制热敏打印机输出
特别要说明的是,虽然Android本身提供了蓝牙API,但通过Qt的QBluetooth抽象层,我们可以用同一套C++代码兼容不同操作系统。实测发现,Qt的蓝牙通信延迟比Android原生API低40%左右。
2.2 通信协议适配
市面主流蓝牙标签打印机都支持ESC/POS指令集,这个1980年代由Epson制定的标准至今仍是热敏打印的通用语言。一个典型的打印指令序列如下:
cpp复制// 初始化打印机
QByteArray cmd;
cmd.append(0x1B); // ESC
cmd.append(0x40); // @
// 设置居中打印
cmd.append(0x1B);
cmd.append(0x61);
cmd.append(0x01);
// 打印文本
cmd.append("药品名称:阿莫西林");
cmd.append("\n"); // 换行
3. 完整实现流程
3.1 开发环境配置
首先需要在Qt Creator中做好Android开发环境配置:
- 安装JDK 8(注意不要用新版,会有兼容性问题)
- 配置Android SDK和NDK路径
- 在.pro文件中添加蓝牙模块:
qmake复制QT += bluetooth
重要提示:必须确保AndroidManifest.xml中添加了蓝牙权限:
xml复制<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
3.2 设备发现与连接
创建蓝牙设备发现代理:
cpp复制QBluetoothDeviceDiscoveryAgent *discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);
connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered,
[=](const QBluetoothDeviceInfo &device){
if(device.name().contains("Printer")){
m_printerDevice = device;
discoveryAgent->stop();
}
});
discoveryAgent->start();
连接设备时需要特别注意Android 6.0+的位置权限问题。我封装了一个权限检查工具类:
cpp复制bool checkBluetoothPermission(){
QtAndroid::PermissionResult result = QtAndroid::checkPermission("android.permission.ACCESS_FINE_LOCATION");
if(result == QtAndroid::PermissionResult::Denied){
QtAndroid::requestPermissionsSync(QStringList() << "android.permission.ACCESS_FINE_LOCATION");
result = QtAndroid::checkPermission("android.permission.ACCESS_FINE_LOCATION");
}
return result == QtAndroid::PermissionResult::Granted;
}
3.3 打印内容生成
标签内容通常包含:
- 文本信息(药品名称、规格等)
- 条形码/二维码
- 表格线等装饰元素
使用QPainter生成打印图像:
cpp复制QPixmap label(384, 200); // 标准58mm纸宽对应384像素
label.fill(Qt::white);
QPainter painter(&label);
painter.setFont(QFont("Arial", 10));
painter.drawText(10, 20, "药品名称:阿莫西林胶囊");
// 生成条形码
QImage barcode = QZXing::encodeData("6941234567890",
QZXing::EncoderFormat_EAN_13,
QSize(200, 50));
painter.drawImage(92, 30, barcode);
// 转换为打印机指令
QByteArray printData = convertPixmapToESC(pos, label);
4. 实战问题与解决方案
4.1 中文乱码问题
早期测试时发现打印机输出中文全是问号,这是因为ESC/POS默认使用代码页437(美国)。解决方案是指定中文代码页:
cpp复制// 设置中文编码
cmd.append(0x1C);
cmd.append(0x26);
4.2 连接稳定性优化
蓝牙连接在Android设备上容易出现异常断开,我通过以下措施提升稳定性:
- 实现心跳包机制:每30秒发送空指令检测连接
- 断线自动重连:监测QBluetoothSocket的error信号
- 发送队列控制:避免同时发送多条指令
cpp复制void PrinterManager::sendData(const QByteArray &data){
if(m_socket->state() != QBluetoothSocket::ConnectedState){
m_pendingData.append(data);
tryReconnect();
return;
}
if(m_isSending){
m_pendingData.append(data);
return;
}
m_isSending = true;
qint64 bytesWritten = m_socket->write(data);
if(bytesWritten == -1){
qWarning() << "Write error:" << m_socket->errorString();
}
}
4.3 打印速度优化
通过实测对比发现,以下方法可以显著提升打印速度:
- 图像预处理:将彩色图像转换为1位深度黑白图
- 数据压缩:使用ESC/POS的压缩指令(0x1D 0x4B)
- 批量发送:合并多个小指令为一个大数据包
cpp复制QByteArray compressImageData(const QImage &image){
QByteArray compressed;
compressed.append(0x1D);
compressed.append(0x4B);
compressed.append(char(0x21)); // 压缩模式
// 每行数据前加长度标识
for(int y=0; y<image.height(); y++){
QByteArray lineData;
for(int x=0; x<image.width(); x+=8){
char byte = 0;
for(int bit=0; bit<8; bit++){
if(image.pixelColor(x+bit, y) == Qt::black){
byte |= (1 << (7-bit));
}
}
lineData.append(byte);
}
compressed.append(char(lineData.size() & 0xFF));
compressed.append(lineData);
}
return compressed;
}
5. 性能对比与实测数据
在Redmi Note 9 Pro上测试不同方案的打印耗时(单位:毫秒):
| 操作项 | Java方案 | Qt方案 | 提升幅度 |
|---|---|---|---|
| 设备发现 | 1200 | 800 | 33% |
| 连接建立 | 1500 | 900 | 40% |
| 50x30mm标签打印 | 2300 | 800 | 65% |
| 断线恢复 | 3000 | 1500 | 50% |
测试条件:打印内容包含2行文本+1个EAN13条码,重复100次取平均值
6. 扩展功能实现
6.1 多标签模板支持
通过JSON配置文件定义不同标签模板:
json复制{
"template1": {
"width": 384,
"height": 200,
"fields": [
{
"type": "text",
"content": "${drugName}",
"x": 10,
"y": 20,
"font": {"family":"Arial", "size":10}
},
{
"type": "barcode",
"content": "${barcode}",
"x": 92,
"y": 30,
"format": "EAN_13"
}
]
}
}
6.2 打印预览功能
使用QGraphicsScene实现WYSIWYG预览:
cpp复制void PrintPreview::renderTemplate(const QJsonObject &tpl){
m_scene->clear();
int width = tpl["width"].toInt();
int height = tpl["height"].toInt();
// 绘制标签边框
m_scene->addRect(0, 0, width, height, QPen(Qt::black));
foreach(auto field, tpl["fields"].toArray()){
QJsonObject obj = field.toObject();
if(obj["type"] == "text"){
QString text = parsePlaceholders(obj["content"].toString());
QGraphicsTextItem *item = m_scene->addText(text);
item->setPos(obj["x"].toInt(), obj["y"].toInt());
}
// 其他元素类型处理...
}
}
7. 部署注意事项
- 蓝牙配对问题:部分打印机需要先在系统设置中配对,才能被Qt发现
- DPI适配:不同打印机每毫米对应的像素数不同,需要做尺寸转换
- 电量管理:Android系统会在后台限制蓝牙通信,建议添加前台服务
- 固件兼容性:测试发现某些国产打印机对ESC/POS指令支持不完整
在项目实际落地过程中,最耗时的不是技术实现,而是处理各型号打印机的兼容性问题。建议在项目初期就收集所有可能用到的打印机型号,建立测试矩阵:
| 打印机型号 | 固件版本 | 中文支持 | 状态检测 | 备注 |
|---|---|---|---|---|
| 佳博GP-2120TU | V2.1.8 | ✓ | ✓ | 推荐型号 |
| 芯烨XP-58B | V1.0.3 | ✓ | ✗ | 无法获取状态 |
| 爱普生TM-T20II | V1.2 | ✗ | ✓ | 需额外字库 |
这个项目最终在30多家药店稳定运行,日均打印标签超过2000张。最大的收获是认识到:在嵌入式设备交互领域,协议层的健壮性比界面美观更重要。后来我们甚至为打印机开发了固件升级工具,通过蓝牙直接推送更新,这又是另一个有趣的技术故事了。