如何使用 CANN DVPP 对图像进行等比例缩放?
摘要:介绍如何用昇腾AI处理器上的DVPP单元进行,图像的等比例缩放,保证图像不变形。
本文分享自华为云社区《CANN DVPP进行图片的等比例缩放》,作者:马城林 。
1. 为什么需要进行等比例的缩放,直接暴力缩放成模型需要的宽高岂不是更省事
首先没有任何的规定表示我们必须进行等比例的缩放,因为实际上即使图像上的物体因为缩放而变形,物体本身的特征还是存在,神经网络依旧可以提取对应的特征进而预测出物体的位置,通过计算实际的宽高与模型之间的宽高的比例依旧可以将模型预测到的候选框的映射到实际的原图上。这样看来等比例的缩放存在感好低!
但是事情真的是这样吗?肯定不是呀,要不我为啥会写这篇博客呢?
首先有以下几点考虑:
- 目标检测除了将图像送入模型,实际上物体在图像中的位置也被作为一部分信息参与了模型的训练,经过等比例缩放的图片在坐标转换上明显省事。
- 有些图像经过非等比例压缩后已经与实际场景中的物体差别很大,严重的变形会干扰到模型的准确性。
- 等比例缩放更加符合我们人类的直觉。
2. 如何在昇腾AI处理器上进行等比例缩放
如果在我们的训练服务器,或者我们自己的个人计算机上,因为计算能力的相对充沛,我们可以借助很多开源图像库例如opencv,pillow等进行图像的等比例缩放。但是一旦道路端侧部署时这些开源图像库的方法往往成为整个推理过程最耗时的部分,因为他们主要通过CPU进行运算,而在诸如200DK这样的环境上,进行这样的变换让本就不充沛的CPU算力雪上加霜。而昇腾AI处理器上拥有专门处理数字图像的硬件单元。所以如何借助这些专用的硬件单元是十分重要的。话不多说开始吧!
3. 实战讲解
主要从以下几个方面进行讲解
- 步骤分解
- 每一步在昇腾AI处理器上的实现
- 怎么把几步组合起来完成一个完整的动作
- 介绍 AscendCL 为开发者提供的组合API接口,只需要一个函数完成所有动作,想想都刺激
<概念注意>
- 昇腾AI处理器在不同的环境上拥有不同运行模式,常见的分为两种,一种是类似于显卡一样插在服务器的PCIE插槽上,提供加速服务,一种是类似于200DK一样的嵌入式环境。
- 工作在服务器的PCIE插槽上的昇腾310内存有两种存在形态,一种就是我们常见的内存,称为host内存。一种类似于显卡的显存,称为device内存,这种状态下需要涉及到内存拷贝的问题。
- 200DK这种环境,不区分host与device,只有一块device内存,所以并不涉及host与device的内存拷贝问题
- 媒体数据处理描述图形的大小,主要有两种表示方法,width,height,表示图像的真实宽高,widthstride,heightstride表示图像在内存中对其到128,16,2后的宽高,内存宽高标识图像在内存中的位置(但是可能有为了填充而产生的无效数据),真实宽高就标识内存中真实有效的数据。
- JPEG Decode时对JPEG的源图像没有宽高的要求,但是输出的数据,宽会对齐到128,长会对齐到16,对齐后可能会产生一部分的无效数据。
- VPC 图像缩放,抠图,贴图对输入的源图像宽高对齐到2,对齐后的内存宽高为,宽对齐到16,高对齐到2,输出与输入约束条件相同
- JPEG Encode 对输入的源图像宽高对齐到2,对齐后的内存宽高为,宽对齐到16或者16的倍数(128时性能更好),高对齐到16。
详细的约束对齐规则
JPEG Decode
VPC
JPEG Encode
3.1 步骤分解
很多朋友和我一样属于一听就会,一动手就废的类型,在脑海里勾勒出万里*,实际一上手,我是个*。。。。。
实际上是我们还没有掌握做事情的诀窍,就是把事情分解成可以很方便用代码描述出的步骤;计算机编程让人感到头皮发麻的一个重要原因就是计算机它只会一步一步来,不会跳步。所以我们脑海里面很简单的步骤在编程实现后也可能代码很长。
1、将常见的JPEG图片解码成YUV格式的图片
aclError acldvppJpegDecodeAsync(acldvppChannelDesc *channelDesc, const void *data, uint32_t size, acldvppPicDesc *outputDesc,aclrtStream stream)
2、因为JPEG解码有128x16的对齐要求,所以解码后的数据边缘会出现无效数据,需要进行裁剪
aclError acldvppVpcCropAsync(acldvppChannelDesc *channelDesc, acldvppPicDesc *inputDesc, acldvppPicDesc *outputDesc, acldvppRoiConfig *cropArea, aclrtStream stream)
3、进行resize的操作,没有对应的等比例缩放的接口,但是如果我们最后指定的图像大小相对于原图是等比例的,就是等比例缩放
aclError acldvppVpcResizeAsync(acldvppChannelDesc *channelDesc, acldvppPicDesc *inputDesc, acldvppPicDesc *outputDesc, acldvppResizeConfig *resizeConfig, aclrtStream stream)
4、进行等比例缩放时需要一个,与模型大小一致的纯色背景图片作为贴图的背景,我们可以申请对应大小的一些区域然后,将对应区域全部赋值为同一个数。
aclError aclrtMemset (void *devPtr, size_t maxCount, int32_t value, size_t count)
5、进行贴图
aclError acldvppVpcCropAndPasteAsync(acldvppChannelDesc *channelDesc, acldvppPicDesc *inputDesc, acldvppPicDesc *outputDesc, acldvppRoiConfig *cropArea, acldvppRoiConfig *pasteArea, aclrtStream stream)
6、进行模型推理或编码保存
3.2 对应的变量怎么声明与初始化
- DVPP处理的通道的声明与销毁
DVPP的一系列操作都是建立在已经声明初始化的通道上,你可以理解为初始化了相关的硬件资源
acldvppChannelDesc *channelDesc
channelDesc = acldvppCreateChannelDesc();
aclError ret = acldvppCreateChannel(channelDesc);
// 销毁与创建的顺序相反
acldvppDestroyChannel(channelDesc);
acldvppDestroyChannelDesc(channelDesc);
- 各种图像的操作描述对应的图像的描述信息的创建与销毁
acldvppPicDesc *picDesc
picDesc = acldvppCreatePicDesc();
acldvppSetPicDescData(picDesc, decodeOutDevBuffer_); // 图像的内存地址
acldvppSetPicDescSize(picDesc, decodeOutBufferSize); // 图像所占的内存大小
acldvppSetPicDescFormat(picDesc, PIXEL_FORMAT_YUV_SEMIPLANAR_420); // 图像的格式
acldvppSetPicDescWidth(picDesc, inputWidth_); // 图像的实际宽
acldvppSetPicDescHeight(picDesc, inputHeight_); // 图像的实际高
acldvppSetPicDescWidthStride(picDesc, decodeOutWidthStride); // 图像在内存中对齐后的宽
acldvppSetPicDescHeightStride(picDesc, decodeOutHeightStride); // 图像在内存中对齐后的高
// 销毁
acldvppDestroyPicDesc(picDesc);
- 申请内存
// device侧,200DK等只有device内存的也用此接口,且只用此接口
aclError aclrtMalloc(void **devPtr, size_t size, aclrtMemMallocPolicy policy)
aclError aclrtFree(void *devPtr)
// host侧
aclError aclrtMallocHost(void **hostPtr, size_t size)
aclError aclrtFreeHost(void *hostPtr)
// dvpp 内存(在device侧,因为需要内存地址128对齐,所以拥有单独的接口)
aclError acldvppMalloc(void **devPtr, size_t size)
aclError acldvppFree(void *devPtr)
- 创建抠图,贴图的区域
acldvppRoiConfig *acldvppCreateRoiConfig(uint32_t left, uint32_t right, uint32_t top, uint32_t bottom)
// 销毁
aclError acldvppDestroyRoiConfig(acldvppRoiConfig *roiConfig)
- 不知道图片的相关信息获取信息的接口
// 通过此接口可以方便的获取JPEG图像的宽,高,色彩通道数,编码格式,只需要传入对应的JPEG源数据地址以及大小
aclError acldvppJpegGetImageInfoV2(const void *data, uint32_t size, uint32_t *width, uint32_t *height, int32_t *components, acldvppJpegFormat *format)
// 获取JPEG图片解码后的数据大小,因为解码后会对齐到128,所以不能任何时候都简单地依据原图大小计算解码后占用的内存大小。
aclError acldvppJpegPredictDecSize(const void *data, uint32_t dataSize, acldvppPixelFormat outputPixelFormat, uint32_t *decSize)
// 获取PNG图像的宽和高
aclError acldvppPngGetImageInfo(const void *data, uint32_t dataSize, uint32_t *width, uint32_t *height, int32_t *components)
// 预测PNG图片解码后占用内存空间
aclError acldvppPngPredictDecSize(const void *data, uint32_t dataSize, acldvppPixelFormat outputPixelFormat, uint32_t *decSize)
// 获取JPEG图像编码后的占用内存空间
aclError acldvppJpegPredictEncSize(const acldvppPicDesc *inputDesc, const acldvppJpegeConfig *config, uint32_t *size)
- 图像缩放,贴图,抠图相关的接口(当有多个操作动作时建议使用组合接口,虽然看似是多个步骤,实际上硬件内部只是执行一次操作可以大幅度提升处理的效率)
// 缩放接口,单一接口,将缩放后的图片作为输出
aclError acldvppVpcResizeAsync(acldvppChannelDesc *channelDesc, acldvppPicDesc *inputDesc, acldvppPicDesc *outputDesc, acldvppResizeConfig *resizeConfig, aclrtStream stream)
// 抠图接口,单一接口,从原图中根据ROI区域抠出一部分作为输出
aclError acldvppVpcCropAsync(acldvppChannelDesc *channelDesc, acldvppPicDesc *inputDesc, acldvppPicDesc *outputDesc, acldvppRoiConfig *cropArea, aclrtStream stream)
// 组合接口(抠图+缩放),应用场景:JPEG 解码后的图片中可能有无效的数据,直接缩放到模型需要的大小会把无效数据一起输入,可以先把真实图像的数据抠出来然后进行缩放,减少无效的数据干扰。
aclError acldvppVpcCropResizeAsync(acldvppChannelDesc *channelDesc, acldvppPicDesc *inputDesc, acldvppPicDesc *outputDesc, acldvppRoiConfig *cropArea, acldvppResizeConfig *resizeConfig, aclrtStream stream)
// 组合接口(抠图+贴图 / 抠图+缩放+贴图)如果抠图与贴图的大小不一致会进行一步缩放操作。
aclError acldvppVpcCropAndPasteAsync(acldvppChannelDesc *channelDesc, acldvppPicDesc *inputDesc, acldvppPicDesc *outputDesc, acldvppRoiConfig *cropArea, acldvppRoiConfig *pasteArea, aclrtStream stream)
// 组合接口(抠图+贴图 / 抠图+缩放+贴图)此接口功能与上一个是一致的,但是此接口可以指定不同的缩放算法
aclError acldvppVpcCropResizePasteAsync(acldvppChannelDesc *channelDesc, acldvppPicDesc *inputDesc, acldvppPicDesc *outputDesc, acldvppRoiConfig *cropArea, acldvppRoiConfig *pasteArea, acldvppResizeConfig *resizeConfig, aclrtStream stream)
3.3 实战讲解
效果展示:1024x688 =》 416x416
// 定义一些全局变量
namespace {
std::string aclConfig = "../src/acl.json";
const std::string image_path = "../data/dog2.jpg";
const std::string image_save_path = "../out/dog2.jpg";
int32_t deviceId = 0;
aclrtContext context;
aclrtStream stream;
aclrtRunMode runMode_ = ACL_DEVICE;
uint32_t model_width = 416; // 模型需要的图像宽
uint32_t model_height = 416; // 模型需要的图像高
uint32_t image_width = 0; // 图像的真实宽
uint32_t image_height = 0; // 图像的真实高
acldvppChannelDesc *dvppChannelDesc = nullptr;
}
开始进行解码操作
// 将图片读入内存,如果运行模式为(ACL_HOST)模式,需要申请host侧内存存放我们的原始图片数据,然后申请相同大小的device内存拷贝过去。如果运行在(ACL_DEVICE)模式,例如200DK这样的场景,直接申请device内存,不需要进行内存的拷贝搬运。
// 经过读取图片的步骤我们应该能获得如下变量
void* JpegImagePtr; // 原始图像在device侧的内存地址(不管运行在那种方式下的昇腾软件栈都是如此)
uint32_t JpegImageSize; // 原始图像的大小
// 我们不了解图像的编码以及真实宽高,还有颜色通道数,所以需要执行获取图像信息的接口
// 图像的原始宽高变量我们前面已经定义过, 所以我们现在只需要定义图像的通道数与编码方式还有图像解码后所占用的内存的大小
int32_t components; // 图像的通道数
acldvppJpegFormat format; // 图像的编码方式
uint32_t JpegdOutSize;
void* JpegdOutPtr;
aclError ret = acldvppJpegGetImageInfoV2(JpegImagePtr, JpegImageSize, &image_width, &height, &components, &format)
aclError ret = acldvppJpegPredictDecSize(JpegImagePtr, JpegImageSize, format, &JpegdOutSize)
// 申请存放解码后YUV图像的内存
aclError ret = acldvppMalloc(&JpegdOutPtr, JpegdOutSize);
// 设置JPEG解码后图像的描述信息(注意JPEG解码后内存宽高对齐到128x16)
acldvppPicDesc* JpegdOutputPicDesc = acldvppCreatePicDesc();
acldvppSetPicDescData(out_pic_desc, JpegdOutPtr);
acldvppSetPicDescSize(out_pic_desc, JpegdOutSize);
acldvppSetPicDescFormat(out_pic_desc, PIXEL_FORMAT_YUV_SEMIPLANAR_420);
acldvppSetPicDescWidth(out_pic_desc, image_width);
acldvppSetPicDescHeight(out_pic_desc, image_height);
acldvppSetPicDescWidthStride(out_pic_desc, ALIGN_UP128(image_width));
acldvppSetPicDescHeightStride(out_pic_desc, ALIGN_UP16(image_height));
// 执行解码接口
ret = acldvppJpegDecodeAsync(dvppChannelDesc, JpegImagePtr, JpegImageSize, JpegdOutputPicDesc, stream);
ret = aclrtSynchronizeStream(stream_);
开始进行抠图的操作,因为我们这次需要执行抠图+缩放+贴图三个动作所以我们选择组合API进行编程
// 首先声明原图抠图的区域
// 抠图的区域左偏移和上偏移都是偶数
// 抠图的区域右偏移和下偏移都是奇数
uint32_t left = 0;
uint32_t top = 0;
uint32_t right = image_width % 2 == 0 ? image_width-1 : image_width;
uint32_t bottom = image_height % 2 == 0 ? image_height-1 : image_height;
acldvppRoiConfig* cropArea = cropConfig = acldvppCreateRoiConfig(left, right, top, bottom);
// 计算等比例缩放后的新的宽高(等比例缩放后可能不满足16x2的对齐规则,需要手动进行对齐的操作)
void LargeSizeAtLeast(uint32_t W, uint32_t H, uint32_t &newInputWidth, uint32_t &newInputHeight)
{
INFO_LOG("W:%u H:%u nw:%u, nh:%u", W, H, newInputWidth, newInputHeight);
float scaleRatio = 0.0;
float inputWidth = 0.0;
float inputHeight = 0.0;
float resizeMax = 0.0;
bool maxWidthFlag = false;
inputWidth = (float)W;
inputHeight = (float)H;
resizeMax = (float)(416);
maxWidthFlag = (W >= H) ? true : false;
if (maxWidthFlag == true) {
newInputWidth = resizeMax;
scaleRatio = resizeMax / W;
// 高度2对齐
newInputHeight = scaleRatio * H;
newInputHeight = ALIGN_UP2(newInputHeight);
INFO_LOG("scaleRatio: %.3f, modelInputWidth: %u, modelInputHeight: %d, newInputWidth: %d, newInputHeight: %d",
scaleRatio, W, H, newInputWidth, newInputHeight);
} else {
scaleRatio = resizeMax / H;
// 如果高度是长边,建议宽度在等比例缩放后再做一次16对齐。因为vpc在输出时宽有16字节对齐约束,当贴图的宽非16对齐时,会导致在贴图的时候,
// 芯片会自动进行16字节对齐,导致每次写入数据的时候都会引入部分无效数据,从而导致精度下降。
newInputWidth = scaleRatio * W;
newInputWidth = ALIGN_UP16(newInputWidth);
newInputHeight = resizeMax;
INFO_LOG("scaleRatio: %.3f, modelInputWidth: %u, modelInputHeight: %d, newInputWidth: %d, newInputHeight: %d",
scaleRatio, W, H, newInputWidth, newInputHeight);
}
}
// 设置贴图的范围左偏移要求16对齐
acldvppRoiConfig *InitVpcOutConfig(uint32_t width, uint32_t height, uint32_t modelInputWidth, uint32_t modelInputHeight)
{
uint32_t right = 0;
uint32_t bottom = 0;
uint32_t left = 0;
uint32_t top = 0;
uint32_t left_stride;
acldvppRoiConfig *cropConfig;
uint32_t small = width < height ? width : height;
uint32_t padded_size_half;
if (small == width) {
padded_size_half = (modelInputWidth - width) / 2; // 贴图区域距离左边界的距离
left = padded_size_half;
left_stride = ALIGN_UP16(left);
right = (left_stride + width) % 2 == 0 ? (left_stride + width - 1) : (left_stride + width);
if (left_stride + right > modelInputWidth) {
while (true) {
left_stride = left_stride - 16;
right = (left_stride + width) % 2 == 0 ? (left_stride + width - 1) : (left_stride + width);
if (left_stride + right < modelInputWidth)
break;
}
}
right = (left_stride + width) % 2 == 0 ? (left_stride + width - 1) : (left_stride + width);
bottom = (modelInputHeight % 2 == 0 ? modelInputHeight - 1 : modelInputHeight);
top = bottom - height + 1;
} else {
padded_size_half = (modelInputHeight - height) / 2;
right = (modelInputWidth % 2 == 0 ? modelInputWidth - 1 : modelInputWidth);
left = right + 1 - width;
left_stride = ALIGN_UP16(left);
top = (padded_size_half % 2 == 0 ? padded_size_half : padded_size_half + 1);
bottom = (height + top - 1) % 2 == 0 ? (height + top - 2) : (height + top - 1);
}
INFO_LOG("left_stride=%d, right=%d, top=%d, bottom=%d\n", left_stride, right, top, bottom);
cropConfig = acldvppCreateRoiConfig(left_stride, right, top, bottom);
if (cropConfig == nullptr) {
ERROR_LOG("acldvppCreateRoiConfig failed");
return nullptr;
}
return cropConfig;
}
// 设置最后完成等比例缩放后的图像的信息
void* output_ptr = nullptr;
uint32_t output_size = YUV420SP_SIZE(ALIGN_UP16(model_width), ALIGN_UP2(model_height));
ret = acldvppMalloc(&output_ptr, output_size); // 申请模板内存
if (ret != ACL_SUCCESS) {
ERROR_LOG("acl malloc output is failed");
}
ret = aclrtMemset(output_ptr, output_size, 128, output_size);
if (ret != ACL_SUCCESS) {
ERROR_LOG("mem set 128 is failed");
}
acldvppPicDesc *output_Desc = acldvppCreatePicDesc();
acldvppSetPicDescData(output_Desc, output_ptr);
acldvppSetPicDescSize(output_Desc, output_size);
acldvppSetPicDescFormat(output_Desc, PIXEL_FORMAT_YUV_SEMIPLANAR_420);
acldvppSetPicDescWidth(output_Desc, model_width);
acldvppSetPicDescHeight(output_Desc, model_height);
acldvppSetPicDescWidthStride(output_Desc, ALIGN_UP16(model_width));
acldvppSetPicDescHeightStride(output_Desc, ALIGN_UP2(model_height));
// 执行裁剪加贴图的操作
ret = acldvppVpcCropAndPasteAsync(dvppChannelDesc, JpegdOutputPicDesc, output_Desc, cropArea, pasteArea, stream);
完成了一系列的操作
总结
- 对于DVPP处理来说,单独的接口调用需要全部满足输入和输出的长宽以及对齐的要求
- 但是对于 JPEG+VPC 这样的串联编程来说前一个组件的输出图像描述是后一个组件的输入描述,设定对齐内存是为了有效标定,图像在内存中的位置,如果固执的设定对齐后的值,对于后一个组件来说很有可能得到的输入是有缺失的信息。
- 根据众多对齐要求我们可以看出,对于图像的原始宽高全部2对齐是一个比较好的习惯,而且常用的图像规格都是偶数例如1920x1080,1280x720, 640x640, 416x416,最好不要出现长或者宽为奇数的情况,因为硬件会自动为奇数进行对齐从而产生无效数据。增加了处理的难度。
点击关注,第一时间了解华为云新鲜技术~
推荐阅读
-
php 如何使用等比例缩放图像示例代码
-
如何使用 CANN DVPP 对图像进行等比例缩放?
-
[OpenCV 手把手教你使用 OpenCV 对 44 幅图像进行超缩放处理
-
[OpenCV 手把手教你使用 OpenCV 对 44 幅图像进行超缩放处理
-
Grid++Report 锐浪报表开发常见问题解答集锦-报表设计 问:怎样在设计时打印预览报表? 答:为了及时查看报表的设计效果,Grid++Report 报表设计应用程序提供了四种查看视图:普通视图、页面视图、预览视图与查询视图。通过窗口下边的 Tab 按钮可以在四种视图中任意切换。在预览视图中查看报表的打印预览效果,在查询视图中查看报表的查询显示效果。如果在报表的记录集提供了数据源连接串与查询 SQL,在进入预览视图与查询视图时会利用数据源连接串与查询 SQL 从数据源中自动取数,否则 Grid++Report 将自动生成模拟数据进行模拟打印预览与查询显示。注意:在预览视图与查询视图中看到的报表运行结果有可能与在你程序中的最终运行结果有差异,因为在报表的生成过程中我们可以在程序中对报表的生成行为进行一定的控制。 问:怎样用 Grid++Report 设计交叉表? 答:Grid++Report 没有提供专门实现交叉表的功能,其它的报表构件提供的交叉表功能一般也比较死板和功能有限。利用 Grid++Report 的编程接口可以做出灵活多变,功能丰富的交叉表。示例程序 CrossTab 就是一个实现交叉表的例子程序,认真领会此例子程序,你就可以做出自己想要各种交叉表,并能提取一些共用代码,便于重复使用。 问:怎样设置整个报表的缺省字体? 答:设置报表主对象的字体属性,也就是设置了整个报表的缺省字体。如果改变报表主对象的字体属性,则没有专门的设置字体属性的子对象的字体属性也跟随改变。同样每个报表节与明细网格也有字体属性,他们的字体属性也就是其拥有的子对象的缺省字体。 问:怎样在打印时限制一页的输出行数? 答:设定明细网格的内容行的‘每页行数(RowsPerPage)’属性即可。另外要注意‘调节行高(AdjustRowHeight)’属性值:为真时根据页面的输出高度自动调整行的高度,使整个页面的输出区域充满。为假时按设计时的高度输出行。 问:怎样显示中文大写金额? 答:将对象的“格式(Format)”属性设为 “$$” 及可,可以设置格式的对象有:字段(IGRField)、参数(IGRParameter)、系统变量(IGRSystemVarBox)与综合文字框(IGRMemoBox),其中综合文字框是在报表式上设格式。 问:能否实现自定义纸张与票据打印? 答:Grid++Report 完全支持自定义纸张的打印,只要在报表设定时在页面设置中选定自定义纸张,并指定准确的纸张尺寸。当然要在最终输出时得道合适的打印结果,输出打印机必须支持自定义纸张打印。Windows2000/XP/2003 操作系统上可以在打印机上定义自定义纸张,也可以采用这种方式实现自定义纸张打印。 问:怎样实现 0 值不打印? 答:直接设置格式串就可以,在“数字格式”设置对话框中选定“0 不显示”,就会得到合适的格式串。也可以通过直接录入格式串来指定 0 不显示,但格式串必须符合 Grid++Report 的规定格式。另一种实现办法是在报表获取明细记录数据时,在 BeforePostRecord 事件中将值为零的字段设为空,调用字段的 Clear 方法将字段置为空。 问:怎样实现多栏报表? 答:在明细网格上设‘页栏数(PageColumnCount)’属性值大于 1 即可。通过 Grid++Report 的“页栏输出顺序”还可以指定多栏报表的输出顺序是“先从上到下”还是“先从左到右”。 问:如何实现票据套打? 答:Grid++Report 为实现票据套打做了很多专门的安排:报表设计器提供了页面设计模式,按照设定的纸张尺寸显示设计面板,如果将空白票据的扫描图设为设计背景图,在定位报表内容的输出位置会非常方便。报表部件可以设定打印类别,非套打输出的内容在套打打印模式下就不会输出。 问:Grid++Report 有没有横向分页功能? 答:回答是肯定的,在列的总宽度超过打印页面的输出宽度时,Grid++Report 可以另起新页输出剩余的列,如果左边存在锁定列,锁定列可以在后面的新页中重复输出,这样可以保证关键数据列在每一页都有输出。仔细体会 Grid++Report 提供的多种打印适应策略,选用最合适的方式。Grid++Report 的多种打印适应策略为开发动态报表提供了很好的支持。 问:怎样实现报表本页小计功能? 答:定义一个报表分组,将本分组定义为页分组,在本分组的分组头与分组尾上定义统计。页分组就是在每页产生一个分组项,在每页的上端与下端都会分别显示页分组的分组头与分组尾,页分组不用定义分组依据字段。 报表运行 问:怎样与数据库建立连接? 答:如果在设计报表时指定了数据集的数据源连接串与查询 SQL 语句,Grid++Report 采用拉模式直接从数据源取得报表数据,Grid++Report 利用 OLE DB 从数据源取数,OLE DB 提供了广泛的数据源操作能力。如果 Grid++Report 的数据来源采用推模式,即 Grid++Report 不直接与数据库建立连接,各种编程语言/平台都提供了很好的数据库连接方式,并且易于操作,应用程序在报表主对象(IGridppReport)的 FetchRecord 事件中将数据传入,例子程序提供了各种编程语言填入数据的通用方法,对C++Builder 和 Delphi 还进行了专门的包装,直接关联 TDataSet 对象也可以将 TDataSet 对象中的数据传给报表。 问:打印时能否对打印纸张进行自适应?支持表格的折行打印吗? 答:Grid++Report 在打印时采用多种适应策略,通过设置明细网格(IGRDetailGrid)的‘打印策略(PrintAdaptMethod)’属性指定打印策略。(1)丢弃:按设计时列的宽度输出,超出范围的内容不显示。(2)绕行:按设计时列的宽度输出,如果在当前行不能完整输出,则另起新行进行输出。(3)缩放适应:对所有列的输出宽度进行按比例地缩放,使总宽度等于页面的输出宽度。(4)缩小适应:如果列的总宽度小于页面的输出宽度,对所有列的输出宽度进行按比例地缩小,使总宽度等于页面的输出宽度。(5)横向分页:超范围的列在新页中输出。(6)横向分页并重复锁定列。 问:如何改变缺省打印预览窗口的窗口标题? 答:改变报表主对象的‘标题(Title)’属性即可。 问:利用集合对象的编程接口取子对象的接口引用,但不是自己期望的结果。 答:Grid++Report中所有集合对象的下标索引都是从 1 开始,另按对象的名称查找对象的接口引用时,名称字符是不区分大小写的。 问:怎样在运行时控制报表中各个对象的可见性?即怎样在运行时显示或隐藏对象? 答:在报表主对象(GridppReport)的 SectionFormat 事件中设定相应报表子对象的可见(Visible)属性即可。 问:报表主对象重新载入数据,设计器中为什么没有反映新载入的数据? 答:应调用 IGRDesigner 的 Reload 方法。 问:怎样实现不进入打印预览界面,直接将报表打印出来?