在 Openwrt 上推送流量统计
在Openwrt上推送流量统计情况
前言
近一两年PCDN逐渐发展起来,有能力有资源的厂商通过将普通用户的带宽资源收集并整合,从而替代一部分昂贵的CDN资源,同时,厂商给与普通用户一定的激励。
抱着白赚硬件和赚回每个月带宽费的想法,我入坑了某厂的硬件,并在后续又上了另一个类似的项目。至此,家里的上传带宽得以充分利用。于是每天早上起来都要看一下昨天又“赚”到了多少“收益”,过了过了。原本只是为了赚个网费而已,却搞得好像买了股票一般。
这类项目每天的奖励是跟上传流量挂钩的,正好加上那段时间开始尝试使用python语言来解决一些实际问题,于是趁着练手的热度决定写一个脚本将家里每天的网络流量推送给我。推送服务同样选择了 Server酱 ,使用简单方便。
准备工作
Wrtbmon作为流量数据的来源
我并不想写一个监控流量的程序记录流量情况,直接使用了 luci-wrtbwmon 插件,该插件安装完成后,能直接通过Openwrt的菜单访问到实时流量监控与统计页面。我通过该插件的说明和实际操作,大概了解到了该插件的一些工作机制(实际是到今天修复一个问题时,才真正又有了进一步的了解,故写下此文)。
-
wrtbmon插件会在安装时会生成usage.db、usage.htm。
-
usage.db文件中就有我想要的各个设备的流量数据。
-
usage.db文件根据设置的不同会有以下路径:
- /tmp/usage.db
- /etc/config/usage.db
-
usage.htm即是访问该插件页面时展示数据的页面。
-
当访问插件页面时,wrtbmon会将数据更新至usage.db文件中,并通过usage.db内的数据更新usage.htm页面。
Wrtbmon插件部分功能源代码
更新页面数据
function usage_data()
local db = usage_database_path()
local publish_cmd = "wrtbwmon publish " .. db .. " /tmp/usage.htm /etc/wrtbwmon.user"
local cmd = "wrtbwmon update " .. db .. " && " .. publish_cmd .. " && cat /tmp/usage.htm"
luci.http.prepare_content("text/html")
luci.http.write(luci.sys.exec(cmd))
end
数据存放位置设置
function usage_database_path()
local cursor = luci.model.uci.cursor()
if cursor:get("wrtbwmon", "general", "persist") == "1" then
return "/etc/config/usage.db"
else
return "/tmp/usage.db"
end
end
重置统计
function usage_reset()
local db = usage_database_path()
local ret = luci.sys.call("wrtbwmon update " .. db .. " && rm " .. db)
luci.http.status(204)
end
usage.db文件内容
root@OpenWrt:/usr/bin/myscripts# cat /etc/config/usage.db
#mac,ip,iface,in,out,total,first_date,last_date
48:89:e7:61:39:8d,192.168.0.136,br-lan,58240270,14814386,73054656,19-02-2021_11:20:48,19-02-2021_14:19:09
00:0c:29:0d:5c:d3,192.168.0.128,br-lan,0,0,0,19-02-2021_11:20:48,19-02-2021_11:20:48
00:0c:29:f8:f9:18,192.168.0.129,br-lan,0,0,0,19-02-2021_11:20:48,19-02-2021_11:20:48
1e:53:61:10:ba:ef,192.168.0.138,br-lan,1388266019,31991293,1420257312,19-02-2021_11:20:48,19-02-2021_14:19:09
86:d2:6a:e1:9a:06,192.168.0.154,br-lan,0,0,0,19-02-2021_11:20:48,19-02-2021_11:20:48
开工
有了数据来源就可以开始干活了。
-
每日备份当日数据
先写一个shell脚本备份当天的usage.db文件后重置统计流量。
#!/bin/ash #update at 2021/02/19 # v0.2 更新了之前发现没有usage.db文件生成,从而导致备份当日数据失败的问题 # 问题在于之前没有搞懂wrtbmon的usage.db文件生成机制。 #睡眠59秒,等到到23:59:59再执行. #需要先通过wrtbmon更新至usage.db sleep 59 #更新数据到usage.db文件 wrtbwmon update /etc/config/usage.db #备份usage.db cp /etc/config/usage.db /usr/bin/myscripts/backup_usage/usage_$(date +"%Y%m%d-%H%M%S").db #重置wrtbmon数据库.将数据导出到usage.db并删除 wrtbwmon update /etc/config/usage.db && rm /etc/config/usage.db sleep 1 # wait 1s ,导出新数据到usage.db,生成新的usage.db文件 wrtbwmon update /etc/config/usage.db
-
写一个Python脚本整理数据并使用Server酱推送
这里我的想法如下:
- 读取usage.db并写入Sqlite数据库,方便我后面数据拿来做别的(比如画个周期的流量图啥的)。
- 从数据库中读取昨日的流量数据并按照Markdown表格格式整理好。
- 通过Server酱推送。
开始写脚本:
#!/usr/bin/env python import requests import sqlite3 import os import datetime #固定参数 path_usagefile = "/usr/bin/myscripts/backup_usage/" #流量记录文件路径 #推送昨日流量信息到server酱微信 def push2wechat(title, content): api = "https://sc.ftqq.com/Server酱KEY.send" # title = u"XX月XX日流量情况汇总" # content = """ # | MAC | 上传流量 | 下载流量 | 总流量 | # | :---------------: | -------- | -------- | ------ | # | AA-AA-AA-AA-AA-AA | 11.0GB | 6.0GB | 17.0GB | # """ data = { "text":title, "desp":content } req = requests.get(url=api, params=data) os.system("echo `date +%y-%m-%d_%H:%M:%S` Pushed sucessfully to wechat! >> push2wechat.log") return req #连接和写入数据到数据库 def init_table_sqlite(): #创建表 conn = sqlite3.connect('/usr/bin/myscripts/usage2push.db') print("openned database sucessfully!") c = conn.cursor() c.execute('''CREATE TABLE DEV_STAT (ID INTEGER PRIMARY KEY NOT NULL, MAC TEXT NOT NULL, IP TEXT NOT NULL, INTERFACE TEXT NOT NULL, DOWN INT NOT NULL, UP INT NOT NULL, TOTAL INT NOT NULL, F_DATE TEXT NOT NULL, L_DATE TEXT NOT NULL);''') print("Table created successfully!") conn.commit() conn.close() #往表里写入数据 def write2sqlite(): #先连接数据库,后面边读边写。 conn = sqlite3.connect('/usr/bin/myscripts/usage2push.db') print("openned database sucessfully!") c = conn.cursor() #检查数据日期是否重复 ye_day = get_yesterday() #对可能存在1位数字的月份和日进行判断,补0位。 str_ye_day = str(ye_day.day) str_ye_month = str(ye_day.month) if len(str_ye_day) == 1: str_ye_day = "0" + str_ye_day if len(str_ye_month) == 1: str_ye_month = "0" + str_ye_month #拼接字符串查找是否已经存在重复数据 sql_str1 = "SELECT * FROM DEV_STAT WHERE F_DATE LIKE '" + str_ye_day + "-" + str_ye_month + "-" + str(ye_day.year) +"%'" c.execute(sql_str1) result_exsite = c.fetchone() if result_exsite !=None: print("该日期数据已存在,不再写入数据库!") os.system("echo `date +%y-%m-%d_%H:%M:%S` Date is exsite in database! >> push2wechat.log")#写入失败日志 else: filename = path_usagefile + "usage_" + str(ye_day.year) + str_ye_month + str_ye_day + "-235959.db" print(filename + " 文件正在打开...") #打开昨日文件 f = open(filename) for line in f.readlines(): line = line.strip() #跳过首行 if line[0] == '#': continue tmp = line.split(',') sql_str_head = 'INSERT INTO DEV_STAT (MAC,IP,INTERFACE,DOWN,UP,TOTAL,F_DATE,L_DATE) VALUES (' sql_str_values = "'" + tmp[0] + "', '" +tmp[1] + "', '" + tmp[2] + "', " + tmp[3] + "," + tmp[4] + "," +tmp[5] + ", '" + tmp[6] + "', '" + tmp[7] + "'" sql_str_end = ');' sql_str2 = sql_str_head + sql_str_values + sql_str_end c.execute(sql_str2) #提交数据 conn.commit() print("write to database successfully!") #关闭连接 conn.close() #计算昨日日期 def get_yesterday(): yesterday = datetime.date.today() + datetime.timedelta(-1) return yesterday def repo(): #从数据库中读取昨日数据 yesterday = get_yesterday() str_ye_day = str(yesterday.day) str_ye_month = str(yesterday.month) if len(str_ye_day) == 1: str_ye_day = "0" + str_ye_day if len(str_ye_month) == 1: str_ye_month = "0" + str_ye_month str_ye = str_ye_day + '-' + str_ye_month + '-' + str(yesterday.year) sql_str = "SELECT * FROM DEV_STAT WHERE F_DATE LIKE " + "'" + str_ye + "%' ORDER BY TOTAL DESC" #连接数据库 conn = sqlite3.connect('/usr/bin/myscripts/usage2push.db') print("openned database sucessfully!") c = conn.cursor() c.execute(sql_str) values = c.fetchall() #构造markdown格式数据 title = str(yesterday.year) + "年" + str_ye_month + "月" + str_ye_day + "日流量情况汇总" content_head = """ | 主机名 | 下载流量 | 上传流量 | 总流量 | | :---------------: | --------: | --------: | ------: | """ content_values = '' for value in values: c.execute("select * from DEV_HOST where MAC = '" + value[1] + "'") result_hostname = c.fetchone() if result_hostname != None: hostname = result_hostname[1] else: hostname = "xx:xx:xx:" + value[1][9:] downdata = value[4]/1024/1024/1024 updata = value[5]/1024/1024/1024 total = value[6]/1024/1024/1024 content_line = "| " + hostname + " |" + str(round(downdata, 1)) + "GB |" + str(round(updata, 1)) + "GB |" + str(round(total, 1)) + "GB | \n" content_values += content_line c.close() content = content_head + content_values print(title) print(content) return (title, content) if __name__ == "__main__": #若无数据库,则初始化数据库 if os.path.exists('/usr/bin/myscripts/usage2push.db') != True: init_table_sqlite() write2sqlite() msg = repo() push2wechat(msg[0], msg[1]) #推送至微信
-
在crontab中添加备份文件和推送的定时任务
这里我设置的每天23点59分启动备份脚本,每天0点2分启动python脚本推送信息,同时将执行脚本的输出信息追加到对应的log文件中,方便发现问题。
59 23 * * * /usr/bin/myscripts/backup_usage.sh >> /usr/bin/myscripts/backup_usage/backup.log 2>&1 & 2 0 * * * /usr/bin/python3.8 /usr/bin/myscripts/push2wechat.py >> /usr/bin/myscripts/push.log 2>&1 &
效果展示
小结
通过这次折腾,让我每天都能收到路由器推送的流量使用情况。
在Python脚本的编写过程成,了解了Sqlite3、Requests模块的简单使用。
原文地址:https://www.cnblogs.com/mustard27/p/PushInfo2wechat.html
推荐阅读
-
epoll简介及触发模式(accept、read、send)-epoll的简单介绍 epoll在LT和ET模式下的读写方式 一、epoll的接口非常简单,一共就三个函数:1. int epoll_create(int size);创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close关闭,否则可能导致fd被耗尽。2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epoll的事件注册函数,它不同与select是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create的返回值,第二个参数表示动作,用三个宏来表示:EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd;第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */};events可以是以下几个宏的集合:EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLIN事件:EPOLLIN事件则只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。现在明白为什么说epoll必须要求异步socket了吧?如果同步socket,而且要求读完所有数据,那么最终就会在堵死在阻塞里。 EPOLLOUT:表示对应的文件描述符可以写; EPOLLOUT事件:EPOLLOUT事件只有在连接时触发一次,表示可写,其他时候想要触发,那要先准备好下面条件:1.某次write,写满了发送缓冲区,返回错误码为EAGAIN。2.对端读取了一些数据,又重新可写了,此时会触发EPOLLOUT。简单地说:EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!其实,如果真的想强制触发一次,也是有办法的,直接调用epoll_ctl重新设置一下event就可以了,event跟原来的设置一模一样都行(但必须包含EPOLLOUT),关键是重新设置,就会马上触发一次EPOLLOUT事件。1. 缓冲区由满变空.2.同时注册EPOLLIN | EPOLLOUT事件,也会触发一次EPOLLOUT事件这个两个也会触发EPOLLOUT事件 EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);EPOLLERR:表示对应的文件描述符发生错误;EPOLLHUP:表示对应的文件描述符被挂断;EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待事件的产生,类似于select调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。-------------------------------------------------------------------------------------------- 从man手册中,得到ET和LT的具体描述如下EPOLL事件有两种模型:Edge Triggered (ET)Level Triggered (LT)假如有这样一个例子:1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符2. 这个时候从管道的另一端被写入了2KB的数据3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作4. 然后我们读取了1KB的数据5. 调用epoll_wait(2)......Edge Triggered 工作模式:如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。 i 基于非阻塞文件句柄 ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。Level Triggered 工作模式相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。然后详细解释ET, LT:LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。(未测试)另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,读数据的时候需要考虑的是当recv返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取: 这里只是说明思路(参考《UNIX网络编程》) while(rs) {buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);if(buflen < 0){// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读// 在这里就当作是该次事件已处理处.if(errno == EAGAIN)break; else return; }else if(buflen == 0) { // 这里表示对端的socket已正常关闭. } if(buflen == sizeof(buf) rs = 1; // 需要再次读取 else rs = 0; } 还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send内部,当写缓冲已满(send返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send内部,但暂没有更好的办法. ssize_t socket_send(int sockfd, const char* buffer, size_t buflen) { ssize_t tmp; size_t total = buflen; const char *p = buffer; while(1) { tmp = send(sockfd, p, total, 0); if(tmp < 0) { // 当send收到信号时,可以继续写,但这里返回-1. if(errno == EINTR) return -1; // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满, // 在这里做延时后再重试. if(errno == EAGAIN) { usleep(1000); continue; } return -1; } if((size_t)tmp == total) return buflen; total -= tmp; p += tmp; } return tmp; } 二、epoll在LT和ET模式下的读写方式 在一个非阻塞的socket上调用read/write函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK) 从字面上看, 意思是: * EAGAIN: 再试一次 * EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block * perror输出: Resource temporarily unavailable 总结: 这个错误表示资源暂时不够, 可能read时, 读缓冲区没有数据, 或者, write时,写缓冲区满了 。 遇到这种情况, 如果是阻塞socket, read/write就要阻塞掉。 而如果是非阻塞socket, read/write立即返回-1, 同 时errno设置为EAGAIN. 所以, 对于阻塞socket, read/write返回-1代表网络出错了. 但对于非阻塞socket, read/write返回-1不一定网络真的出错了. 可能是Resource temporarily unavailable. 这时你应该再试, 直到Resource available. 综上, 对于non-blocking的socket, 正确的读写操作为: 读: 忽略掉errno = EAGAIN的错误, 下次继续读 写: 忽略掉errno = EAGAIN的错误, 下次继续写 对于select和epoll的LT模式, 这种读写方式是没有问题的. 但对于epoll的ET模式, 这种方式还有漏洞. epoll的两种模式 LT 和 ET
-
在 Openwrt 上推送流量统计
-
实时音频和视频技术的发展与应用-1.1 双重音频和视频 从架构上看,双人音视频系统相对简单明了。红点代表房间信令服务,房间信令服务的主要功能是管理房间信息,实现容量协商和上下行链路的质量调节,例如当下行信道发生拥塞时,上行线路的码率和分辨率会降低。 在传输信道层面,我们的策略是优先直连,在跨区域、跨运营商的情况下,我们会选择单中转或双中转信道,在策略上尽量保持直连和中转信道同时存在,当其中一个信道的质量不好时,系统会自动切断到另一个信道的流量。 1.2 多人音视频 多人视频通话的产品形态是整个房间不超过 50 人,大盘平均房间规模约为 4.x 人,房间内部最多满足一个大视频和三个小视频(四屏)。根据这一条件,我们在架构中采用了典型的 SFU 小房间设计。 上图中的红点代表房间信令服务,主要用于房间管理和状态信息同步。房间管理主要包括用户列表的管理,例如哪些用户打开了视频/音频,我看了谁,谁看了我,这些都是基于房间管理的信息,然后房间信令服务会将这些信息同步到媒体传输服务进行数据分发。 房间服务的另一个作用是房间级容量协商和质量控制,例如,房间里的每个人一开始都支持 H.265 编码,当某个时刻进来一个只支持 H.264 编码的用户时,房间里所有的上游主播就必须把 H.265 切成 H.264。还有一种情况是,房间里有一定比例的人下行链路信道质量较差,这会导致上行链路房间质量下降。 在传输层面,我们采用的是单层分布式媒体传输网络,大家都选择中转方式,不区分双人和多人,采用 Full-Mesh 传输机制将所有数据推送过去,比如一个节点上的人并不都看另外两个人的视频,但还是会将视频推送给他们。
-
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 方法。 问:怎样实现不进入打印预览界面,直接将报表打印出来?
-
在Vue项目中实现实时访问流量统计:Matomo的运用