下面我将为您详细拆解这个过程,并提供一个完整的、可运行的示例。

(图片来源网络,侵删)
整个过程可以分为三个主要步骤:
- C语言连接SQL Server:使用一个C语言的数据库连接库(最常用的是 ODBC)来连接到您的SQL Server数据库,并执行SQL查询语句,获取数据。
- 数据处理:将查询到的数据存储在C语言的数据结构中(如结构体数组),进行必要的处理和格式化。
- 生成PDF文件:使用一个C语言的PDF生成库(最常用的是 libharu)来创建一个PDF文档,将处理后的数据以表格或文本的形式写入PDF文件,并最终保存到磁盘。
第一步:环境准备
在开始编码之前,您需要准备好所有必需的软件和库。
SQL Server 环境
确保您有一台可以访问的SQL Server数据库(可以是本地实例或远程服务器),并且有一个测试用的表和数据。
示例表结构:

(图片来源网络,侵删)
-- 在SQL Server中创建一个名为 Customers 的表
CREATE TABLE Customers (
CustomerID INT PRIMARY KEY,
CustomerName NVARCHAR(100),
City NVARCHAR(50),
OrderDate DATE,
Amount DECIMAL(10, 2)
);
-- 插入一些测试数据
INSERT INTO Customers VALUES
(1, '张三', '北京', '2025-10-25', 1500.50),
(2, '李四', '上海', '2025-10-26', 2300.00),
(3, '王五', '广州', '2025-10-27', 899.99),
(4, '赵六', '深圳', '2025-10-28', 4500.00),
(5, '钱七', '北京', '2025-10-29', 3200.75);
C语言开发环境
- 编译器: Visual Studio (Windows下最方便), GCC (Linux/MinGW), Clang 等。
- 包管理器 (推荐):
vcpkg可以极大地简化第三方库的安装。
安装必要的库
a) ODBC 驱动
这是C语言连接SQL Server的桥梁。
- Windows: 微软官方提供了ODBC驱动,最简单的方式是安装 ODBC Driver 17 for SQL Server,安装后,在“ODBC 数据源管理器”中可以配置DSN(数据源名称),但我们的代码将使用更灵活的“连接字符串”方式,所以配置DSN不是必须的。
- Linux (Ubuntu/Debian):
sudo apt-get update sudo apt-get install unixodbc-dev tdsodbc msodbcsql17
msodbcsql17是微软的官方驱动。
b) libharu 库
这是一个用于生成PDF文件的C语言库,功能强大且免费。
- 使用 vcpkg 安装 (推荐):
# 在vcpkg根目录下执行 ./vcpkg install haru
- 手动安装: 从 libharu官网 下载源码,按照其文档进行编译安装。
c) 其他依赖
- Windows: 通常包含在Visual Studio中。
- Linux:
sudo apt-get install build-essential cmake
第二步:C语言代码实现
下面是一个完整的C语言示例代码,它实现了:

(图片来源网络,侵删)
- 连接到SQL Server。
- 执行查询,获取
Customers表的数据。 - 使用
libharu将这些数据生成一个名为report.pdf的PDF文件。
代码文件:generate_report.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sql.h>
#include <sqlext.h> // 包含 SQLExtendedFetch 等扩展函数
// libharu 头文件
#include <hpdf.h>
// 定义客户结构体,用于存储从数据库获取的数据
typedef struct {
int id;
char name[101];
char city[51];
char order_date[11]; // YYYY-MM-DD
double amount;
} Customer;
// ODBC 相关变量
SQLHENV henv; // 环境句柄
SQLHDBC hdbc; // 连接句柄
SQLHSTMT hstmt; // 语句句柄
// libharu 相关变量
HPDF_Doc pdf;
HPDF_Page page;
HPDF_Font font;
HPDF_Font bold_font;
// 函数声明
void connect_to_db();
void disconnect_from_db();
void fetch_data_from_db(Customer **customers, int *count);
void generate_pdf_report(Customer *customers, int count);
void handle_odbc_error(const char *function, SQLHANDLE handle, SQLSMALLINT type);
int main() {
Customer *customers = NULL;
int customer_count = 0;
printf("开始连接数据库...\n");
connect_to_db();
printf("数据库连接成功,\n");
printf("正在从数据库获取数据...\n");
fetch_data_from_db(&customers, &customer_count);
printf("成功获取 %d 条客户记录,\n", customer_count);
if (customer_count > 0) {
printf("正在生成PDF报表...\n");
generate_pdf_report(customers, customer_count);
printf("PDF报表 'report.pdf' 生成成功!\n");
} else {
printf("没有数据可生成报表,\n");
}
// 释放内存
free(customers);
printf("正在断开数据库连接...\n");
disconnect_from_db();
printf("数据库连接已断开,\n");
return 0;
}
// 连接到 SQL Server
void connect_to_db() {
SQLRETURN ret;
// 分配环境句柄
ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv);
if (!SQL_SUCCEEDED(ret)) {
handle_odbc_error("SQLAllocHandle (ENV)", henv, SQL_HANDLE_ENV);
exit(1);
}
// 设置ODBC版本为3.0
SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
// 分配连接句柄
ret = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
if (!SQL_SUCCEEDED(ret)) {
handle_odbc_error("SQLAllocHandle (DBC)", hdbc, SQL_HANDLE_DBC);
exit(1);
}
// 连接字符串 - 请根据您的实际情况修改
// Server=.; 表示本地默认实例
// UID=sa; PWD=your_password; 请替换为您的用户名和密码
SQLCHAR conn_str[] = "DRIVER={ODBC Driver 17 for SQL Server};SERVER=.;DATABASE=YourDBName;UID=sa;PWD=your_password;";
// 连接数据库
ret = SQLDriverConnect(hdbc, NULL, conn_str, SQL_NTS, NULL, 0, NULL, SQL_DRIVER_COMPLETE);
if (!SQL_SUCCEEDED(ret)) {
handle_odbc_error("SQLDriverConnect", hdbc, SQL_HANDLE_DBC);
exit(1);
}
}
// 断开连接
void disconnect_from_db() {
if (hdbc) SQLDisconnect(hdbc);
if (hstmt) SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
if (hdbc) SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
if (henv) SQLFreeHandle(SQL_HANDLE_ENV, henv);
}
// 从数据库获取数据
void fetch_data_from_db(Customer **customers, int *count) {
SQLRETURN ret;
SQLCHAR sql[] = "SELECT CustomerID, CustomerName, City, OrderDate, Amount FROM Customers";
SQLLEN ind_or_len; // 用于获取字符串或二进制数据的长度
// 分配语句句柄
ret = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt);
if (!SQL_SUCCEEDED(ret)) {
handle_odbc_error("SQLAllocHandle (STMT)", hstmt, SQL_HANDLE_STMT);
exit(1);
}
// 执行SQL查询
ret = SQLExecDirect(hstmt, sql, SQL_NTS);
if (!SQL_SUCCEEDED(ret)) {
handle_odbc_error("SQLExecDirect", hstmt, SQL_HANDLE_STMT);
exit(1);
}
// 绑定列到变量
// SQLBindCol 的参数顺序是: 句柄, 列号, C数据类型, 变量地址, 变量长度, 长度/指示器指针
SQLBindCol(hstmt, 1, SQL_C_LONG, &(*customers)[0].id, 0, NULL);
SQLBindCol(hstmt, 2, SQL_C_CHAR, (*customers)[0].name, sizeof((*customers)[0].name), &ind_or_len);
SQLBindCol(hstmt, 3, SQL_C_CHAR, (*customers)[0].city, sizeof((*customers)[0].city), &ind_or_len);
SQLBindCol(hstmt, 4, SQL_C_CHAR, (*customers)[0].order_date, sizeof((*customers)[0].order_date), &ind_or_len);
SQLBindCol(hstmt, 5, SQL_C_DOUBLE, &(*customers)[0].amount, 0, NULL);
// 先获取总行数 (一种简单方法,并非所有驱动都支持,这里我们采用循环获取的方式)
// 更稳健的方法是先分配一个初始数组,然后不够时realloc
// 为了简化,我们这里先查询一次得到行数,然后再分配内存
ret = SQLRowCount(hstmt, (SQLLEN*)count);
if (!SQL_SUCCEEDED(ret) || *count <= 0) {
*count = 0; // 如果无法获取行数或为0,则设为0
}
// 分配内存
*customers = (Customer*)malloc(*count * sizeof(Customer));
if (*customers == NULL) {
fprintf(stderr, "内存分配失败\n");
exit(1);
}
// 重新绑定,因为数组地址变了
SQLBindCol(hstmt, 1, SQL_C_LONG, &(*customers)[0].id, 0, NULL);
SQLBindCol(hstmt, 2, SQL_C_CHAR, (*customers)[0].name, sizeof((*customers)[0].name), &ind_or_len);
SQLBindCol(hstmt, 3, SQL_C_CHAR, (*customers)[0].city, sizeof((*customers)[0].city), &ind_or_len);
SQLBindCol(hstmt, 4, SQL_C_CHAR, (*customers)[0].order_date, sizeof((*customers)[0].order_date), &ind_or_len);
SQLBindCol(hstmt, 5, SQL_C_DOUBLE, &(*customers)[0].amount, 0, NULL);
// 获取数据
int i = 0;
while (SQL_SUCCEEDED(SQLFetch(hstmt))) {
if (i >= *count) { // 防止越界,理论上不会发生
break;
}
// SQLBindCol 已经将数据拷贝到结构体中了,这里只需要递增索引
i++;
}
*count = i; // 更新实际获取的行数
// 释放语句句柄
SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
hstmt = NULL;
}
// 生成PDF报表
void generate_pdf_report(Customer *customers, int count) {
HPDF_Doc pdf;
HPDF_Page page;
HPDF_Font font;
HPDF_Font bold_font;
HPDF_REAL page_height;
HPDF_REAL y_pos;
const char *filename = "report.pdf";
int i;
// 1. 创建PDF文档
pdf = HPDF_New(NULL, NULL);
if (!pdf) {
printf("错误: 无法创建PDF文档\n");
return;
}
// 2. 添加一页
page = HPDF_AddPage(pdf);
HPDF_Page_SetSize(page, HPDF_PAGE_SIZE_A4, HPDF_PAGE_PORTRAIT);
// 3. 加载字体
// libharu自带了Helvetica等字体,Windows系统下也可以加载本地字体
font = HPDF_GetFont(pdf, "Helvetica", NULL);
bold_font = HPDF_GetFont(pdf, "Helvetica-Bold", NULL);
if (!font || !bold_font) {
printf("错误: 无法加载字体\n");
HPDF_Free(pdf);
return;
}
page_height = HPDF_Page_GetHeight(page);
// 4. 绘制内容
y_pos = page_height - 50; // 从顶部50像素开始
// 标题
HP_Page_SetTextLeading(page, 20);
HPDF_Page_SetFontAndSize(page, bold_font, 18);
HPDF_Page_BeginText(page);
HPDF_Page_TextOut(page, 50, y_pos, "客户销售报表");
HPDF_Page_EndText(page);
y_pos -= 40; // 标题下移
// 表头
HPDF_Page_SetFontAndSize(page, bold_font, 12);
HPDF_Page_BeginText(page);
HPDF_Page_TextOut(page, 50, y_pos, "ID");
HPDF_Page_TextOut(page, 100, y_pos, "客户名称");
HPDF_Page_TextOut(page, 220, y_pos, "城市");
HPDF_Page_TextOut(page, 300, y_pos, "订单日期");
HPDF_Page_TextOut(page, 420, y_pos, "金额");
HPDF_Page_EndText(page);
y_pos -= 20; // 表头下移
// 绘制一条横线
HPDF_Page_SetLineWidth(page, 1);
HPDF_Page_MoveTo(page, 50, y_pos);
HPDF_Page_LineTo(page, 550, y_pos);
HPDF_Page_Stroke(page);
y_pos -= 5; // 线条下移,为数据留出空间
// 表格数据
HPDF_Page_SetFontAndSize(page, font, 12);
for (i = 0; i < count; i++) {
HPDF_Page_BeginText(page);
char amount_str[32];
sprintf(amount_str, "%.2f", customers[i].amount);
HPDF_Page_TextOut(page, 50, y_pos, customers[i].id);
HPDF_Page_TextOut(page, 100, y_pos, customers[i].name);
HPDF_Page_TextOut(page, 220, y_pos, customers[i].city);
HPDF_Page_TextOut(page, 300, y_pos, customers[i].order_date);
HPDF_Page_TextOut(page, 420, y_pos, amount_str);
HPDF_Page_EndText(page);
y_pos -= 20; // 每行数据下移
// 检查是否需要换页
if (y_pos < 50) {
HPDF_Page_EndText(page);
page = HPDF_AddPage(pdf);
y_pos = page_height - 50;
HPDF_Page_SetFontAndSize(page, font, 12);
HPDF_Page_BeginText(page);
}
}
// 5. 保存PDF文件
HPDF_SaveToFile(pdf, filename);
HPDF_Free(pdf);
}
// ODBC 错误处理函数
void handle_odbc_error(const char *function, SQLHANDLE handle, SQLSMALLINT type) {
SQLCHAR sqlstate[6], message[SQL_MAX_MESSAGE_LENGTH];
SQLINTEGER native;
SQLSMALLINT len, i = 1;
fprintf(stderr, "错误在函数: %s\n", function);
while (SQLGetDiagRec(type, handle, i, sqlstate, &native, message, sizeof(message), &len) == SQL_SUCCESS) {
fprintf(stderr, "SQLSTATE: %s, Message: %s\n", sqlstate, message);
i++;
}
exit(1);
}
第三步:编译与运行
在 Windows (Visual Studio) 中编译
- 创建一个新的C++空项目(即使你写的是C代码,用C++项目编译兼容性更好)。
- 将
generate_report.c文件添加到项目中。 - 配置项目属性:
- 配置属性 -> 链接器 -> 输入 -> 附加依赖项:
添加
odbc32.lib和odbccp32.lib。 - 配置属性 -> C/C++ -> 常规 -> 附加包含目录:
如果libharu是手动编译的,添加其
include目录路径。 - 配置属性 -> 链接器 -> 常规 -> 附加库目录:
添加libharu的
lib目录路径。
- 配置属性 -> 链接器 -> 输入 -> 附加依赖项:
添加
- 确保你的项目配置为使用 Release 模式,并选择正确的平台(如 x64)。
- 按
F5或Ctrl+F5编译并运行。
在 Linux (GCC) 中编译
假设你使用了 vcpkg 安装了依赖,编译命令会非常简单。
# 编译命令
# -I 指定头文件目录
# -L 指定库文件目录
# -l 指定要链接的库
# $(pkg-config --cflags --libs odbc) 会自动帮你找到ODBC的编译和链接参数
gcc generate_report.c -o generate_report \
-I/path/to/vcpkg/installed/x64-linux/include \
-L/path/to/vcpkg/installed/x64-linux/lib \
-lharu \
$(pkg-config --cflags --libs odbc)
# 运行
./generate_report
注意: 请将 /path/to/vcpkg/... 替换为你实际的vcpkg安装路径。
运行结果
成功运行程序后,你会在程序所在目录下找到一个名为 report.pdf 的文件,用PDF阅读器打开它,你会看到一个格式良好的客户销售报表。
总结与最佳实践
- 错误处理: 代码中的
handle_odbc_error是一个基本的错误处理模板,在生产环境中,你需要更健壮的错误处理和日志记录机制。 - 内存管理: 使用
malloc分配的内存,一定要记得用free释放,避免内存泄漏。 - 安全性: 连接字符串中直接包含用户名和密码,在实际应用中,应该使用更安全的方式(如配置文件、环境变量或密钥管理服务)来存储敏感信息。
- PDF高级功能:
libharu支持更多高级功能,如:- 表格线: 绘制完整的表格边框。
- 图片: 将Logo等图片嵌入PDF。
- 页眉页脚: 添加页码、公司名称等。
- 字体: 支持加载TrueType字体(如宋体、黑体)以更好地支持中文。
- 加密: 设置PDF的打开密码和权限密码。
这个完整的示例为你提供了一个坚实的基础,你可以根据实际业务需求进行修改和扩展。
