ReportMachineDemo 项目分析¶
项目信息¶
- 项目名称: ReportMachineDemo
- 下载链接: ReportMachineDemo.rar
- 所属类别: 报表 (ReportMachine)
- 分析时间: 2026-10-15
- 核心组件: RMReport、RMDesigner、RMCompositeReport、RMDBDataSet、RMHTMExport、RMXLSExport
运行截图¶

文件结构¶
ReportMachineDemo/
├── Extractor_Icon.ico
├── Project1.py # 工程主程序
├── Project1.xml
├── uDemoData.py # 演示1~4 的数据集(客户、订单主从、商品)
├── uMain.py # 主窗体:按钮事件、报表的预览/设计/打印/导出
├── uMain.pydfm # 二进制窗体属性
├── uMain.sct # 事件绑定脚本
├── uMain.sfm # 文本窗体定义
├── uSampleData.py # 6 个样例模板用的数据集(rmds1~rmds7)
├── Templates/
│ └── Samples/
│ ├── accident.rmf # 样例:事故登记
│ ├── cash.rmf # 样例:现金日记账
│ ├── elderdetail.rmf # 样例:老人明细(含图片)
│ ├── elderfeetable.rmf # 样例:老人费用表
│ ├── inware.rmf # 样例:入库单
│ ├── outware.rmf # 样例:出库单
│ ├── photo.bmp # 占位照片
│ └── stock_d.rmf # 样例:库存
主程序文件: Project1.py¶
from glcl import *
from uMain import *
def main():
Application.Initialize()
Application.Title = 'Project1'
Application.MainFormOnTaskbar = True
Application.Icon.LoadFromFile(os.path.join(
os.path.dirname(os.path.abspath(__file__)), "Extractor_Icon.ico"))
MainForm = frmMain(Application)
MainForm.Show()
FreeConsole()
Application.Run()
MainForm.Free()
if __name__ == '__main__':
main()
单元文件: uMain.py¶
# Powered By Python Studio, The best Python GUI IDE to download from glsite.com.
# -*- coding: utf-8 -*-
"""
uMain.py 主窗体: 按钮事件、报表的预览/设计/打印/导出
uDemoData.py 演示1~4 用的数据集(客户、订单主从、商品)
uSampleData.py 6 个样例模板用的数据集(rmds1~rmds7)
【重要经验】Python 回调 OnGetValue 会干扰报表取数
曾经给每个报表都挂了 Python 版 OnGetValue(为演示3 的 [ReportTitle] 等变量服务), 结果样例1、样例5
的数据单元格全部变成空白; 样例预览不再挂这个回调后恢复正常. 原因推测是 RM 对每个值求值时都会回调它,
而 var 参数的写回会破坏某些值. 所以现在只有真正需要变量的演示3 才挂它, 见 NewReport.
"""
import os
import re
import time
from glcl import *
import uDemoData
from uDemoData import keep, build_customer_ds, build_order_item_ds, build_product_ds
from uSampleData import (make_photo_file, build_inware_ds, build_outware_ds,
build_elder_fee_ds, build_elder_detail_ds, build_cash_ds,
build_accident_ds)
# 开关: 针对"样例模板"页的两项修补
ENABLE_TEMPLATE_PATCH = True # 预览/打印样例前, 对 inware.rmf、elderdetail.rmf 做"等长替换"补丁
ENABLE_SCRIPT_PATCH = True # 清空样例模板脚本里调用 GetValue/GetFieldValue 的过程(样例4、样例5)
# ===========================================================================
# 常量
# ===========================================================================
# 样例页 6 个模板的文件名, 顺序与界面上的样例1~样例6 对应
SAMPLE_FILES = ['inware.rmf', 'outware.rmf', 'accident.rmf',
'cash.rmf', 'elderdetail.rmf', 'elderfeetable.rmf']
# 内置演示1~4 的模板文件名(需要用户在设计器里自己做并保存)
TEMPLATE_NAMES = {1: 'demo1_customer.rmf', 2: 'demo2_order_detail.rmf',
3: 'demo3_variables.rmf', 4: 'demo4_group.rmf'}
# 演示3 才使用报表变量 [ReportTitle] 等, 只有它需要挂 OnGetValue 回调
VARIABLE_DEMO = 3
# OnGetValue 里 [ReportTitle] 按演示编号返回的标题
REPORT_TITLES = {1: '客户清单', 2: '订单明细(主从)', 3: '客户清单(变量与过滤演示)', 4: '商品分组汇总'}
# 操作代码: 0 预览, 1 设计, 2 打印
ACT_PREVIEW, ACT_DESIGN, ACT_PRINT = 0, 1, 2
# --- 样例模板脚本修补(ENABLE_SCRIPT_PATCH) ----------------------------------
# 样例4(cash.rmf)、样例5(elderdetail.rmf)的脚本调用了 RM 内置函数 GetValue / GetFieldValue,
# 在测试环境里脚本编译器报 "Undeclared Identifier". PatchScript 用下面三个正则找出并清空这类过程.
_PROC_RE = re.compile(r'^procedure\s+(\w+)\s*(\([^)]*\))?\s*;(.*?)^end;', re.I | re.S | re.M)
_BAD_CALL_RE = re.compile(r'\b(GetFieldValue|GetValue)\s*\(', re.I)
_LOADFILE_RE = re.compile(r'(\w+)\s*\.\s*LoadFromFile\s*\(', re.I)
# --- 样例模板文件补丁(ENABLE_TEMPLATE_PATCH) ---------------------------------
# 在加载前对 .rmf 文件做"等长替换"生成临时副本(原模板不改动), 去掉模板里的数值运算表达式:
# inware.rmf Sum([cnt] * [price]) -> Sum([sum]) (数据里 sum 字段本来就等于 cnt*price)
# elderdetail.rmf IF([字段]=数字, ...) -> 读数据集里预先算好的文字字段 instate_name 等
# "等长": 新文本比旧文本短时, 在该备忘录文本末尾(\r\n 之前)补空格, 这样 .rmf 的二进制长度字段不受影响,
# 显示上也看不出来. 每项: (字节正则, 替换字节串, 期望命中次数); 命中次数不对就不打补丁.
FILE_PATCH_RULES = {
'inware.rmf': [
# \xe5\x85\x83 是"元"的 UTF-8 编码; 两个汇总备忘录(分组尾)各出现一次
(rb'\[Sum\(\[rmds1\."cnt"\] \* \[rmds1\."price"\]\)\]\xe5\x85\x83(?=\r\n)',
b'[Sum([rmds1."sum"])]\xe5\x85\x83', 2),
],
'elderdetail.rmf': [
(rb'\[IF\(\[rmds5\."instate"\]=0,[^\r\n]*\)\](?=\r\n)', b'[rmds5."instate_name"]', 1),
(rb'\[IF\(\[rmds5\."gender"\]=0,[^\r\n]*\)\](?=\r\n)', b'[rmds5."gender_name"]', 1),
(rb'\[IF\(\[rmds5\."marrige"\]=0,[^\r\n]*\)\](?=\r\n)', b'[rmds5."marrige_name"]', 1),
],
}
# ---------------------------------------------------------------------------
# 右侧帮助面板文字
# 键: 0 欢迎页, 1~4 内置演示, 5 预生成报表, 6 导出, 8 样例模板, 9 高级功能
# ---------------------------------------------------------------------------
HELP_TEXTS = {
1: [
'【演示1 客户清单 - 单表报表】',
'数据集: rmCustomer',
'字段: CustNo, CustName, City, Contact, Phone, Credit',
'',
'设计步骤:',
'1. 点 [设计] 打开设计器',
'2. 插入"报表标题"带, 放文本框写: 客户清单',
'3. 插入"页眉"带, 放几个文本框做列标题',
'4. 插入"主数据(MasterData)"带, 属性 DataSet 选 rmCustomer',
'5. 在数据带内放文本框, 内容填 [rmCustomer."CustName"] 等字段',
' (也可以从设计器的数据字段列表拖入)',
'6. 插入"页脚"带, 放页码系统变量',
'7. 关闭设计器, 选择保存到 Templates 目录',
'之后点 [预览] / [打印] 即可.',
],
2: [
'【演示2 订单主从明细】',
'主数据集: rmOrder (OrderNo, CustNo, OrderDate, Status, Total)',
'明细数据集: rmOrderItem (OrderNo, LineNo, ProdName, Qty, Price, Amount)',
'',
'设计步骤:',
'1. 插入"主数据"带, DataSet 选 rmOrder, 放订单号/日期/状态/合计',
'2. 在主数据带下面插入"明细数据(DetailData)"带,',
' DataSet 选 rmOrderItem, 放商品/数量/单价/金额',
'3. 主从关系已在 ClientDataSet 上通过 MasterSource/MasterFields',
' (OrderNo)建立好, 报表里不需要再设置连接.',
'4. 保存模板后预览, 每个订单下面会跟着它自己的明细行.',
],
3: [
'【演示3 变量与过滤】',
'数据集: rmCustomer, 先在左侧选好"过滤城市"再预览',
'',
'本例通过 OnGetValue 提供以下变量, 在文本框里直接写即可:',
' [ReportTitle] 报表标题',
' [Company] 公司名称',
' [PrintUser] 当前 Windows 用户名',
' [PrintTime] 打印时间',
' [FilterInfo] 过滤条件说明',
' [RecCount] 记录数',
'启用 全局变量 [GlobalNote].',
'',
'设计: 标题带放 [ReportTitle] 和 [Company],',
'页脚放 [PrintUser] [PrintTime] [RecCount],',
'主数据带 DataSet 选 rmCustomer, 放客户字段.',
],
4: [
'【演示4 商品分组汇总】',
'数据集: rmProduct (Category, ProdNo, ProdName, Price, Qty, Amount)',
'数据已按 Category 排序.',
'',
'设计步骤:',
'1. 插入"分组头(GroupHeader)"带, 分组条件填 [rmProduct."Category"]',
'2. 在分组头放文本框显示 [rmProduct."Category"]',
'3. 插入"主数据"带, DataSet 选 rmProduct, 放商品明细',
'4. 插入"分组尾(GroupFooter)"带, 放汇总文本框统计 Amount',
' (网样例里的写法: [Sum([数据集."字段"],主数据带名])',
' 也可以用"计算文本框"并设置汇总类型)',
'5. 插入"报表总结"带放全部合计',
],
5: [
'【演示5 预生成报表 .rmp】',
'把演示1的报表先 PrepareReport 生成结果页, 保存成 .rmp;',
'之后无需数据集, 可以直接载入并预览, 适合归档/存档报表.',
'',
'先做好演示1的模板, 点 [生成保存], 再点 [载入预览].',
'文件: Templates\\demo1_prepared.rmp',
],
6: [
'【演示6 导出】',
'导出的是演示1的模板, 输出到 Templates 目录.',
],
8: [
'【样例模板】',
'共 6 个真实使用中的 .rmf, 涵盖:',
' - 多级分组头/分组尾 + Sum 汇总 + IF/Copy 表达式',
' - 报表脚本(OnBeforePrint 隐藏行, 动态加载图片)',
' - 网格报表页(TRMGridReportPage)',
'',
'数据集名称 rmds1/2/4/5/6/7 由本程序生成同名同字段的',
'模拟数据, 所以模板不需要修改即可显示.',
'点 [设计] 可以在设计器里查看它们是怎么做的, 是很好的学习材料.',
'',
'勾选"用代码改写标题"后, 程序会在 LoadFromFile 之后用',
"FindObject('Memo1').Memo.Text := ... 修改模板里的文本框.",
],
9: [
'【高级功能】',
'打开任意 .rmf: 用来测试你自己的模板.',
' 你的模板里数据集名字要与本程序数据集一致,',
' 或者在设计器里把带区的 DataSet 改选为本程序的数据集.',
'',
' 内部使用 TRMCompositeReport, 把多个报表合并到一个预览.',
],
0: [
'【ReportMachine 多功能演示 v2】',
'',
'页1 内置演示: 4 个报表 + 预生成报表 + 导出',
'页2 样例模板: 6 个真实 .rmf 直接加载',
'页3 高级功能: 打开任意 rmf、多报表合并',
'',
'数据全部由内存 TClientDataSet 生成, 不需要数据库.',
'',
'使用流程(页1): 点 [设计] 做模板并保存, 再 [预览]/[打印].',
'模板保存在程序同目录的 Templates 文件夹.',
],
}
# 窗体上全部按钮的名称. 它们共用同一个 OnClick 处理函数 BtnClick, 由按钮的 Tag 区分
BUTTON_NAMES = [
'btnDemo1Design',
'btnDemo1Preview',
'btnDemo1Print',
'btnDemo2Design',
'btnDemo2Preview',
'btnDemo2Print',
'btnDemo3Design',
'btnDemo3Preview',
'btnDemo3Print',
'btnDemo4Design',
'btnDemo4Preview',
'btnDemo4Print',
'btnPrepSave',
'btnPrepLoad',
'btnExpHtml',
'btnExpXls',
'btnOpenDir',
'btnSample1Preview',
'btnSample1Design',
'btnSample1Print',
'btnSample2Preview',
'btnSample2Design',
'btnSample2Print',
'btnSample3Preview',
'btnSample3Design',
'btnSample3Print',
'btnSample4Preview',
'btnSample4Design',
'btnSample4Print',
'btnSample5Preview',
'btnSample5Design',
'btnSample5Print',
'btnSample6Preview',
'btnSample6Design',
'btnSample6Print',
'btnOpenAny',
'btnComposite',
]
def quoted_str(s):
"""等价于 Delphi 的 QuotedStr: 用单引号括起来, 内部的单引号写成两个. 用于拼 Filter 表达式."""
return "'" + s.replace("'", "''") + "'"
class frmMain(Form):
"""主窗体. 界面控件由 uMain.pydfm 描述, 报表对象在代码里动态创建."""
def __init__(self, owner):
# ---- 控件声明 ----
self.memLog = Memo(self)
self.memHelp = Memo(self)
self.pnlClient = Panel(self)
self.pcMain = PageControl(self)
self.tsBuiltin = TabSheet(self)
self.lblDemo1 = Label(self)
self.lblDemo2 = Label(self)
self.lblDemo3 = Label(self)
self.lblCity = Label(self)
self.lblDemo4 = Label(self)
self.lblDemo5 = Label(self)
self.lblDemo6 = Label(self)
self.lblTplDir = Label(self)
self.btnDemo1Design = Button(self)
self.btnDemo1Preview = Button(self)
self.btnDemo1Print = Button(self)
self.btnDemo2Design = Button(self)
self.btnDemo2Preview = Button(self)
self.btnDemo2Print = Button(self)
self.btnDemo3Design = Button(self)
self.btnDemo3Preview = Button(self)
self.btnDemo3Print = Button(self)
self.cbCity = ComboBox(self)
self.btnDemo4Design = Button(self)
self.btnDemo4Preview = Button(self)
self.btnDemo4Print = Button(self)
self.btnPrepSave = Button(self)
self.btnPrepLoad = Button(self)
self.btnExpHtml = Button(self)
self.btnExpXls = Button(self)
self.btnOpenDir = Button(self)
self.tsSamples = TabSheet(self)
self.lblSample1 = Label(self)
self.lblSample2 = Label(self)
self.lblSample3 = Label(self)
self.lblSample4 = Label(self)
self.lblSample5 = Label(self)
self.lblSample6 = Label(self)
self.btnSample1Preview = Button(self)
self.btnSample1Design = Button(self)
self.btnSample1Print = Button(self)
self.btnSample2Preview = Button(self)
self.btnSample2Design = Button(self)
self.btnSample2Print = Button(self)
self.btnSample3Preview = Button(self)
self.btnSample3Design = Button(self)
self.btnSample3Print = Button(self)
self.btnSample4Preview = Button(self)
self.btnSample4Design = Button(self)
self.btnSample4Print = Button(self)
self.btnSample5Preview = Button(self)
self.btnSample5Design = Button(self)
self.btnSample5Print = Button(self)
self.btnSample6Preview = Button(self)
self.btnSample6Design = Button(self)
self.btnSample6Print = Button(self)
self.chkTitle = CheckBox(self)
self.edtTitle = Edit(self)
self.tsAdvanced = TabSheet(self)
self.lblOpenAny = Label(self)
self.lblComposite = Label(self)
self.lblDatasets = Label(self)
self.btnOpenAny = Button(self)
self.btnComposite = Button(self)
self.LoadProps(os.path.join(os.path.dirname(os.path.abspath(__file__)), "uMain.pydfm"))
# ---- 事件绑定 ----
# sfm 里按钮没有 OnClick, 在这里统一绑定到 BtnClick
for name in BUTTON_NAMES:
getattr(self, name).OnClick = self.BtnClick
# ---- 成员变量 ----
self.FReport = None # 当前的 RMReport. 每次操作都重新创建, 见 NewReport
self.FDesigner = None # 报表设计器组件
self.FCust = None # 客户数据集(演示1/3 用, 演示3 会按城市过滤)
self.FOrder = None # 订单主表
self.FItem = None # 订单明细表(与主表联动)
self.FProd = None # 商品数据集(演示4 分组汇总)
self.FCurIdx = 0 # 当前演示编号, OnGetValue 里据此返回不同的标题
self.FPhoto = '' # 样例5 用的占位照片路径
self.FHtmlExp = None # HTML 导出器
self.FXlsExp = None # Excel 导出器
self.FTplDir = '' # 模板目录(带结尾反斜杠)
self.FSampleDir = '' # 样例模板目录
self.FormCreate()
# ------------------------------------------------------------------
# 窗体创建
# ------------------------------------------------------------------
def FormCreate(self):
"""初始化: 确定模板目录、创建设计器、生成演示数据、显示欢迎帮助."""
# 模板目录放在脚本所在目录下. Delphi 用 exe 所在目录, 但 Python 里 Application.ExeName
# 是 python.exe, 所以改用 __file__.
base = os.path.dirname(os.path.abspath(__file__))
self.FTplDir = os.path.join(base, 'Templates') + os.sep
self.FSampleDir = os.path.join(self.FTplDir, 'Samples') + os.sep
os.makedirs(self.FSampleDir, exist_ok=True)
# 设计器组件: 报表的 DesignReport 需要它; TemplateDir 决定"打开/保存"对话框的默认目录
self.FDesigner = RMDesigner(self)
self.FDesigner.TemplateDir = self.FTplDir
self.BuildData()
self.ShowHelp(0)
self.Log('启动完成. 模板目录: ' + self.FTplDir)
def BuildData(self):
"""生成全部演示数据, 并为每个数据集包一层 RM 数据集(RMDBDataSet)供报表引用."""
# 演示1~4 用的数据集
self.FCust = build_customer_ds(self)
self.FOrder, self.FItem = build_order_item_ds(self)
self.FProd = build_product_ds(self)
# RM 数据集的 Name 就是设计器和模板里看到的名字, 如 [rmCustomer."CustName"]
self.MakeRmDS('rmCustomer', self.FCust)
self.MakeRmDS('rmOrder', self.FOrder)
self.MakeRmDS('rmOrderItem', self.FItem)
self.MakeRmDS('rmProduct', self.FProd)
# 样例模板固定使用 rmds1 ~ rmds7 这几个名字(没有 rmds3), 字段名也要与模板一致
photo = self.FSampleDir + 'photo.bmp'
make_photo_file(photo) # 照片文件不存在时生成一个占位图
self.FPhoto = photo
self.MakeRmDS('rmds1', build_inware_ds(self))
self.MakeRmDS('rmds2', build_outware_ds(self))
self.MakeRmDS('rmds4', build_elder_fee_ds(self))
self.MakeRmDS('rmds5', build_elder_detail_ds(self, photo))
self.MakeRmDS('rmds6', build_cash_ds(self))
self.MakeRmDS('rmds7', build_accident_ds(self))
# 导出器: 报表 PrepareReport 之后用 ExportTo 导出为 HTML / Excel
self.FHtmlExp = RMHTMExport(self)
self.FXlsExp = RMXLSExport(self)
def MakeRmDS(self, name, ds):
"""创建一个 RM 数据集并关联到 ClientDataSet. 返回值用 keep() 保存引用, 防止 Python 端过早回收."""
result = keep(RMDBDataSet(self))
result.Name = name
result.DataSet = ds
return result
def Log(self, s):
"""向左下角的日志框追加一行, 带时间戳."""
self.memLog.Lines.Add(time.strftime('%H:%M:%S ') + s)
def ShowHelp(self, idx):
"""在右侧帮助框里显示第 idx 号帮助; 没有对应内容时显示欢迎页."""
self.memHelp.Lines.Clear()
for line in HELP_TEXTS.get(idx, HELP_TEXTS[0]):
self.memHelp.Lines.Add(line)
# ------------------------------------------------------------------
# 报表核心逻辑
# ------------------------------------------------------------------
def TemplateFile(self, idx):
"""演示 idx 的模板文件完整路径."""
return self.FTplDir + TEMPLATE_NAMES[idx]
def ApplyFilter(self, idx):
"""按演示编号设置客户数据集的过滤条件: 只有演示3 并且选了具体城市时才过滤."""
self.FCust.Filtered = False
self.FCust.Filter = ''
if idx == VARIABLE_DEMO and self.cbCity.ItemIndex > 0: # 第 0 项是"全部城市"
self.FCust.Filter = 'City = ' + quoted_str(self.cbCity.Text)
self.FCust.Filtered = True
self.FCust.First()
def NewReport(self, idx, handler=None):
"""释放旧报表并新建一个 RMReport, 避免上一次的模板内容残留.
handler: 是否挂接 OnGetValue 回调. 默认只有演示3(要用 [ReportTitle] 等变量)才挂.
不需要变量的报表一律不要挂: 这个 Python 回调曾导致样例1、样例5 的数据全部变成空白
"""
if handler is None:
handler = (idx == VARIABLE_DEMO)
self.FCurIdx = idx
if self.FReport is not None:
try:
self.FReport.Free()
except Exception:
pass
self.FReport = None
self.FReport = RMReport(self)
if handler:
self.FReport.OnGetValue = self.ReportGetValue
self.ApplyFilter(idx)
def RunFile(self, afile, idx, act, retitle, sample=False):
"""加载模板文件 afile, 然后按 act 执行预览 / 设计 / 打印.
idx 演示编号(决定过滤条件和变量标题); 样例和"打开任意模板"传 0
act ACT_PREVIEW / ACT_DESIGN / ACT_PRINT
retitle 是否在加载后用代码改写模板里 Memo1 的文本(样例页的"用代码改写标题"复选框)
sample 是否是样例页的模板. 样例模板需要做下面的两项修补
"""
# 修补只用于样例的预览/打印. 设计模式必须打开原模板, 否则用户保存时会把修补后的内容写回样例文件.
fix = sample and act != ACT_DESIGN
self.NewReport(idx)
load_file = afile
if fix and ENABLE_TEMPLATE_PATCH:
load_file = self.PatchedTemplate(afile) # 没有补丁规则/打补丁失败时返回原文件
if os.path.isfile(afile):
self.FReport.LoadFromFile(load_file)
if fix and ENABLE_SCRIPT_PATCH:
self.FixPictures(self.PatchScript(self.FReport))
# 演示"加载模板后用代码修改其中的对象": 找到名为 Memo1 的文本框, 改写它的内容
title = self.edtTitle.Text
if retitle and act != ACT_DESIGN and title != '':
v = self.FReport.FindObject('Memo1')
if v is not None:
self.SetMemoText(v, title)
self.Log('已用代码改写 Memo1 文本')
else:
self.Log('模板中没有名为 Memo1 的对象, 未改写')
name = os.path.basename(afile)
if act == ACT_PREVIEW:
self.Log('预览: ' + name)
self.FReport.ShowReport()
elif act == ACT_DESIGN:
self.Log('打开设计器: ' + name)
self.FReport.DesignReport()
# 设计器关闭后询问是否把改动保存回模板文件
if self.Confirm('是否将当前设计保存到:\n' + afile + ' ?'):
self.FReport.SaveToFile(afile)
self.Log('模板已保存: ' + name)
elif act == ACT_PRINT:
self.Log('打印: ' + name)
self.FReport.PrintReport()
# ------------------------------------------------------------------
# 样例模板的修补(详见文件开头的两个开关)
# ------------------------------------------------------------------
def PatchedTemplate(self, afile):
"""按 FILE_PATCH_RULES 对模板文件做等长替换, 把结果写成临时副本并返回副本路径.
没有对应规则、规则命中次数与预期不符(例如模板被用户改过)、或出现异常时, 返回原文件路径,
即"按原样加载". 原模板文件从不被修改.
"""
name = os.path.basename(afile)
rules = FILE_PATCH_RULES.get(name.lower())
if not rules or not os.path.isfile(afile):
return afile
try:
with open(afile, 'rb') as f:
data = f.read()
total = 0
for pattern, new, expected in rules:
def repl(m, new=new):
old = m.group(0)
if len(new) > len(old):
raise ValueError('替换文本比原文长, 无法等长替换')
return new + b' ' * (len(old) - len(new)) # 末尾补空格保持总长度不变
data, n = re.subn(pattern, repl, data)
if n != expected:
self.Log('模板补丁未命中(%s: %d/%d), 按原样加载' % (name, n, expected))
return afile
total += n
out = os.path.join(uDemoData.data_dir(), name) # 临时目录, 程序退出时自动清理
with open(out, 'wb') as f:
f.write(data)
self.Log('已生成模板补丁副本: %s (等长替换 %d 处)' % (name, total))
return out
except Exception as e:
self.Log('模板补丁失败, 按原样加载: %s' % e)
return afile
def PatchScript(self, report):
"""清空模板脚本里调用 GetValue / GetFieldValue 的过程, 避免 RM 弹出 "Undeclared Identifier".
只替换整个过程体为空过程(过程名和参数保持不变, 模板里绑定的事件仍然能找到它).
返回这些过程里出现过 "对象名.LoadFromFile(" 的对象名列表, 交给 FixPictures 用代码补上.
"""
pictures = []
changed = []
try:
src = report.Script.Text
except Exception as e:
self.Log('读取模板脚本失败: %s' % e)
return pictures
def repl(m):
body = m.group(3)
if not _BAD_CALL_RE.search(body):
return m.group(0) # 不含问题调用的过程原样保留
changed.append(m.group(1))
pictures.extend(_LOADFILE_RE.findall(body))
return 'procedure %s%s;\r\nbegin\r\nend;' % (m.group(1), m.group(2) or '')
new_src = _PROC_RE.sub(repl, src)
if changed:
try:
report.Script.Text = new_src
except Exception: # 个别环境下 Text 不可写: 退回逐行写入
report.Script.Clear()
for line in new_src.split('\r\n'):
report.Script.Add(line)
self.Log('已清空模板脚本中的过程: ' + ', '.join(changed))
return pictures
def FixPictures(self, names):
"""给图片对象直接赋图, 代替被清空的脚本.
样例5 的脚本原本在每行打印前取 rmds5."fphoto" 的路径再 LoadFromFile; 由于所有行用的是同一张
照片, 直接给 Picture1 载入这张图即可, 效果相同.
"""
for nm in names:
v = self.FReport.FindObject(nm)
if v is None:
self.Log('模板中没有名为 %s 的图片对象' % nm)
continue
try:
v.Picture.LoadFromFile(self.FPhoto)
self.Log('已为 %s 载入图片 %s' % (nm, os.path.basename(self.FPhoto)))
except Exception as e:
self.Log('为 %s 载入图片失败: %s' % (nm, e))
@staticmethod
def SetMemoText(view, text):
"""等价于 Delphi 的 V.Memo.Text := text. Text 不可写时退回 Clear + Add."""
try:
view.Memo.Text = text
except Exception:
view.Memo.Clear()
view.Memo.Add(text)
def Confirm(self, text):
"""是/否确认框, 点"是"返回 True. """
return Application.MessageBox(text, '确认', MB_YESNO | MB_ICONQUESTION) == IDYES
# ------------------------------------------------------------------
# 各按钮对应的动作
# ------------------------------------------------------------------
def DoTpl(self, idx, act):
"""内置演示 idx 的设计 / 预览 / 打印. 预览和打印时若模板还没做, 提示并可直接进设计器新建."""
fn = self.TemplateFile(idx)
if act != ACT_DESIGN and not os.path.isfile(fn):
if self.Confirm('模板文件不存在:\n' + fn + '\n\n是否现在打开设计器新建?'):
self.RunFile(fn, idx, ACT_DESIGN, False)
return
self.RunFile(fn, idx, act, False)
def DoSample(self, i, act):
"""样例页第 i 个模板(从 0 开始)的预览 / 设计 / 打印. 样例模板需要修补, 所以 sample=True."""
fn = self.FSampleDir + SAMPLE_FILES[i]
self.RunFile(fn, 0, act, self.chkTitle.Checked, True)
def DoOpenAny(self):
"""打开任意 .rmf 文件, 询问是预览还是设计."""
dlg = OpenDialog(self)
try:
dlg.Filter = 'ReportMachine 模板 (*.rmf)|*.rmf|所有文件 (*.*)|*.*'
dlg.InitialDir = self.FTplDir
if not dlg.Execute():
return
fn = dlg.FileName
finally:
dlg.Free()
# 三选一: Yes 预览 / No 设计 / Cancel 取消
r = Application.MessageBox(
'对 ' + os.path.basename(fn) + ' 执行什么操作?\n\n[Yes] 预览 [No] 设计 [Cancel] 取消',
'确认', MB_YESNOCANCEL | MB_ICONQUESTION)
if r == IDYES:
self.RunFile(fn, 0, ACT_PREVIEW, False)
elif r == IDNO:
self.RunFile(fn, 0, ACT_DESIGN, False)
def DoComposite(self):
"""多报表合并预览: 把演示1 和演示2 的模板放进 RMCompositeReport 一起预览."""
t1, t2 = self.TemplateFile(1), self.TemplateFile(2)
if not (os.path.isfile(t1) and os.path.isfile(t2)):
ShowMessage('请先完成演示1和演示2的模板设计.')
return
self.ApplyFilter(0)
r1 = RMReport(self)
r2 = RMReport(self)
c = RMCompositeReport(self)
try:
r1.LoadFromFile(t1)
r2.LoadFromFile(t2)
try:
c.Reports.Clear()
c.Reports.Add(r1)
c.Reports.Add(r2)
use_composite = True
except Exception as e:
# glcl 对 TList.Add 的封装不一定可用. 不可用时退而求其次: 用 AppendFromFile
# 把第二个模板追加到第一个报表里, 同样能在一个预览窗口里看到两份报表.
use_composite = False
self.Log('Reports.Add 不可用(%s), 改用 AppendFromFile 合并' % e)
self.Log('多报表合并预览: 演示1 + 演示2')
if use_composite:
c.ShowReport()
else:
r1.AppendFromFile(t2)
r1.ShowReport()
finally:
# 先组合报表, 再两个子报表
for obj in (c, r2, r1):
try:
obj.Free()
except Exception:
pass
def DoSavePrepared(self):
"""演示5(上): 生成演示1 的报表结果页并保存成 .rmp, 之后无需数据集即可载入预览."""
fn = self.TemplateFile(1)
if not os.path.isfile(fn):
ShowMessage('请先完成演示1的模板设计.')
return
self.NewReport(1)
self.FReport.LoadFromFile(fn)
t = time.perf_counter()
self.FReport.PrepareReport() # 只生成结果页, 不显示
t = int((time.perf_counter() - t) * 1000)
self.FReport.SavePreparedReport(self.FTplDir + 'demo1_prepared.rmp')
self.Log('已生成并保存 demo1_prepared.rmp, 生成耗时 %d ms' % t)
def DoLoadPrepared(self):
"""演示5(下): 载入 .rmp 并预览(不需要模板, 也不需要数据集)."""
fn = self.FTplDir + 'demo1_prepared.rmp'
if not os.path.isfile(fn):
ShowMessage('还没有 .rmp 文件, 请先点 [生成保存].')
return
self.NewReport(1)
self.FReport.LoadPreparedReport(fn)
self.Log('载入预生成报表: demo1_prepared.rmp')
self.FReport.ShowPreparedReport()
def DoExport(self, kind):
"""演示6: 把演示1 的报表导出. kind=0 导出 HTML, 其它导出 Excel. 输出到模板目录."""
fn = self.TemplateFile(1)
if not os.path.isfile(fn):
ShowMessage('请先完成演示1的模板设计.')
return
self.NewReport(1)
self.FReport.LoadFromFile(fn)
self.FReport.PrepareReport() # 导出前必须先生成结果页
if kind == 0:
self.FReport.ExportTo(self.FHtmlExp, self.FTplDir + 'demo1.html')
self.Log('已导出 demo1.html')
else:
self.FReport.ExportTo(self.FXlsExp, self.FTplDir + 'demo1.xls')
self.Log('已导出 demo1.xls')
# ------------------------------------------------------------------
# 按钮事件: 所有按钮共用, 用 Tag 区分
# 10~42 内置演示 十位 = 演示编号(1~4), 个位: 0 设计, 1 预览, 2 打印
# 50/51 预生成报表 保存 / 载入预览
# 60/61 导出 HTML / Excel
# 70 打开模板目录
# 100~152 样例模板 (Tag-100)//10 = 样例序号(0~5), 个位: 0 预览, 1 设计, 2 打印
# 210 打开任意 rmf 220 多报表合并
# ------------------------------------------------------------------
def BtnClick(self, Sender):
tag = Sender.Tag
try:
if tag >= 200:
self.ShowHelp(9)
if tag == 210:
self.DoOpenAny()
elif tag == 220:
self.DoComposite()
elif tag >= 100:
self.ShowHelp(8)
self.DoSample((tag - 100) // 10, (tag - 100) % 10)
else:
idx = tag // 10
act = tag % 10
self.ShowHelp(idx)
if 1 <= idx <= 4:
# 注意: 内置演示里 Tag 个位的含义是 0 设计 / 1 预览 / 2 打印
if act == 0:
self.DoTpl(idx, ACT_DESIGN)
elif act == 1:
self.DoTpl(idx, ACT_PREVIEW)
elif act == 2:
self.DoTpl(idx, ACT_PRINT)
elif idx == 5:
if act == 0:
self.DoSavePrepared()
else:
self.DoLoadPrepared()
elif idx == 6:
self.DoExport(act)
elif idx == 7:
# 在资源管理器中打开模板目录
Application.ShellExecute(0, 'open', self.FTplDir, None, None, SW_SHOWNORMAL)
except Exception as e:
# 任何异常都写进日志并弹框, 不让程序崩溃
self.Log('错误 ' + type(e).__name__ + ': ' + str(e))
Application.MessageBox('运行出错:\n' + type(e).__name__ + '\n' + str(e),
'错误', MB_OK | MB_ICONERROR)
# ------------------------------------------------------------------
# OnGetValue: 演示3 的报表变量
#
# 报表里写 [ReportTitle] / [Company] 等未定义的变量时, RM 会回调这里取值.
# ParValue 对应 Delphi 的 "var ParValue: Variant", 在 Python 里是一个容器对象, 要通过 .Value 回写.
# 只有演示3 才挂这个回调, 原因见文件开头的说明和 NewReport.
# ------------------------------------------------------------------
def ReportGetValue(self, ParName, ParValue):
name = ParName.lower() # 变量名不区分大小写
if name == 'reporttitle':
ParValue.Value = REPORT_TITLES.get(self.FCurIdx, 'ReportMachine 演示')
elif name == 'company':
ParValue.Value = '演示科技有限公司'
elif name == 'printuser':
ParValue.Value = os.environ.get('USERNAME', '') # 当前 Windows 用户名
elif name == 'printtime':
ParValue.Value = time.strftime('%Y-%m-%d %H:%M:%S')
elif name == 'filterinfo':
if self.FCust.Filtered:
ParValue.Value = '城市 = ' + self.cbCity.Text
else:
ParValue.Value = '全部城市'
elif name == 'reccount':
ParValue.Value = self.FCust.RecordCount
elif name == 'globalnote':
# Delphi 版把它放在全局变量 RMVariables 里; glcl 没有导出 RMVariables, 改在这里提供
ParValue.Value = '来自 RMVariables 的全局变量'
数据模块: uDemoData.py¶
# -*- coding: utf-8 -*-
"""演示数据: 全部在内存中生成, 不需要数据库.
对应 Delphi 工程的 uDemoData.pas, 提供演示1~4 使用的三组数据:
客户表 build_customer_ds 演示1(单表)、演示3(变量与过滤)
订单主表+明细表 build_order_item_ds 演示2(主从明细)
商品表 build_product_ds 演示4(分组汇总)
同时提供样例数据模块 uSampleData.py 共用的底层工具:
delphi_random / set_rand_seed 与 Delphi 完全一致的随机数
make_cds 由字段定义+行数据生成并打开一个 ClientDataSet
keep / data_dir 引用保活、临时目录
与 Delphi 版的差异(都是由 glcl 的能力决定的, 数据内容等价):
* glcl 里没有 ftString 这类字段类型常量, 而且 FieldDefs.Add 被封装成了无参数的 TCollection.Add(),
没法按 Delphi 的方式 "FieldDefs.Add + CreateDataSet + Append/Post" 建表. 所以改用 glcl 官方示例
(Demos/DataAccess/DBGridAndDataDemo)里的办法: 先生成 MIDAS 的 DATAPACKET XML 文件,
再通过 ClientDataSet.FileName + Active=True 载入, 得到的就是一个普通的 TClientDataSet.
* 生成的 XML 是纯 ASCII(中文写成 &#NNNN; 数字引用), 这样不管 ClientDataSet 按 UTF-8 还是按系统代码页
读取文件都不会乱码(早期版本直接写 UTF-8, 在测试环境里出现过中文乱码).
* Delphi 的 RandSeed / Random 是一个固定的线性同余算法, 这里原样实现了一份, 所以只要种子相同,
生成的演示数据与 Delphi 版逐值一致.
"""
import os
import atexit
import shutil
import tempfile
from glcl import ClientDataSet, DataSource
# ---------------------------------------------------------------------------
# Delphi 兼容的随机数 (System.Random / RandSeed)
# ---------------------------------------------------------------------------
_rand_seed = 0 # 与 Delphi 的全局变量 RandSeed 对应; 各 build_xxx 函数开头会自己设定种子
def set_rand_seed(value):
"""等价于 Delphi 的 RandSeed := value"""
global _rand_seed
_rand_seed = value & 0xFFFFFFFF
def delphi_random(rng):
"""等价于 Delphi 的 Random(rng), 返回 0..rng-1 的整数.
Delphi 的整数版 Random 是: RandSeed := RandSeed * $08088405 + 1; 结果 := (RandSeed * rng) shr 32
(全部按 32 位无符号运算). 调用顺序也必须与 Delphi 版一致, 否则后面的数据都会错位.
"""
global _rand_seed
_rand_seed = (_rand_seed * 0x08088405 + 1) & 0xFFFFFFFF
return (_rand_seed * rng) >> 32
# ---------------------------------------------------------------------------
# 内存数据集: 生成 DATAPACKET XML -> ClientDataSet
# ---------------------------------------------------------------------------
# 字段类型字符 -> MIDAS XML 的 fieldtype. 'S' 字符串, 'F' 浮点, 'I' 整数, 'D' 日期时间
_XML_TYPES = {'S': 'string', 'F': 'r8', 'I': 'i4', 'D': 'dateTime'}
_data_dir = None # 临时目录, 第一次用到时才创建
_keep = [] # 保存 Python 端对 Delphi 组件的引用, 避免被提前回收
def _get_data_dir():
"""返回(必要时创建)放 XML 数据文件和样例模板补丁副本的临时目录, 程序退出时自动删除."""
global _data_dir
if _data_dir is None:
_data_dir = tempfile.mkdtemp(prefix='RMSample_')
atexit.register(shutil.rmtree, _data_dir, True)
return _data_dir
def data_dir():
"""对外的临时目录入口(uMain 用它存放修补后的模板副本)."""
return _get_data_dir()
def keep(obj):
"""保存对象引用并原样返回. 窗体拥有这些组件, 但 Python 端的包装对象也要有人持有, 防止被回收."""
_keep.append(obj)
return obj
def _xml_attr(text):
"""转成 XML 属性值. 所有非 ASCII 字符写成 &#NNNN; 数字引用, 使整个 XML 文件是纯 ASCII."""
out = []
for ch in str(text):
o = ord(ch)
if ch == '&':
out.append('&')
elif ch == '<':
out.append('<')
elif ch == '>':
out.append('>')
elif ch == '"':
out.append('"')
elif o < 32 or o > 126:
out.append('&#%d;' % o)
else:
out.append(ch)
return ''.join(out)
def _fmt_value(ftype, value):
"""把一个 Python 值格式化成对应字段类型的 XML 文本."""
if ftype == 'F':
return '%.10g' % float(value) # 3200.0 -> '3200', 避免出现科学计数法
if ftype == 'I':
return '%d' % int(value)
if ftype == 'D': # 传入 (年, 月, 日), 写成 MIDAS 的 dateTime 格式
return '%04d%02d%02dT00:00:00000' % value
return _xml_attr(value)
def make_cds(owner, tag, fields, rows):
"""创建并打开一个内存 ClientDataSet.
owner 组件的拥有者(主窗体)
tag 数据集标识, 同时用作临时 XML 文件名
fields [(字段名, 类型字符, 字符串宽度), ...] 宽度只对 'S' 有效
rows [{字段名: 值, ...}, ...] 行里缺省的字段即 NULL
"""
# 字符串字段的宽度不能小于实际内容(例如照片路径可能很长), 否则载入会失败或被截断
widths = {}
for name, ftype, size in fields:
if ftype == 'S':
longest = max([len(str(r[name])) for r in rows if name in r] or [0])
widths[name] = max(size, longest)
# 拼出 MIDAS XML: 先是字段元数据, 再是每一行数据. RowState="4" 表示该行是未修改的原始数据
out = ['<?xml version="1.0" standalone="yes"?>',
'<DATAPACKET Version="2.0">',
'<METADATA>', '<FIELDS>']
for name, ftype, size in fields:
if ftype == 'S':
out.append('<FIELD attrname="%s" fieldtype="string" WIDTH="%d"/>'
% (name, widths[name]))
else:
out.append('<FIELD attrname="%s" fieldtype="%s"/>' % (name, _XML_TYPES[ftype]))
out += ['</FIELDS>', '<PARAMS/>', '</METADATA>', '<ROWDATA>']
for r in rows:
attrs = ''.join(' %s="%s"' % (name, _fmt_value(ftype, r[name]))
for name, ftype, size in fields if name in r)
out.append('<ROW RowState="4"%s/>' % attrs)
out += ['</ROWDATA>', '</DATAPACKET>']
path = os.path.join(_get_data_dir(), tag + '.xml')
with open(path, 'w', encoding='ascii', newline='\n') as f:
f.write('\n'.join(out))
# 载入: FileName 指向 XML 文件, Active=True 触发读取
ds = keep(ClientDataSet(owner))
ds.FileName = path
ds.Active = True
return ds
# ---------------------------------------------------------------------------
# 客户表: CustNo, CustName, City, Contact, Phone, Credit (20 行)
# ---------------------------------------------------------------------------
def build_customer_ds(owner):
cities = ['北京', '上海', '广州', '深圳']
suffix = ['华达贸易', '恒信电子', '远景科技', '宏图实业', '鑫源物流']
contacts = ['张伟', '王芳', '李娜', '刘洋', '陈静', '杨帆']
set_rand_seed(20260929) # 固定种子, 每次启动数据一致
rows = []
for i in range(20):
# 注意随机数的调用顺序: 先电话, 再信用额度(与 Delphi 版的语句顺序一致)
phone = '1380%07d' % delphi_random(10000000)
credit = (delphi_random(50) + 10) * 1000
rows.append({
'CustNo': 1000 + i,
'CustName': cities[i % 4] + suffix[i % 5] + '有限公司',
'City': cities[i % 4],
'Contact': contacts[i % 6],
'Phone': phone,
'Credit': credit,
})
ds = make_cds(owner, 'cdsCustomer', [
('CustNo', 'I', 0), ('CustName', 'S', 40), ('City', 'S', 20),
('Contact', 'S', 20), ('Phone', 'S', 20), ('Credit', 'F', 0)], rows)
ds.IndexFieldNames = 'CustNo' # 按客户号排序
ds.First()
return ds
# ---------------------------------------------------------------------------
# 订单主表: OrderNo, CustNo, OrderDate, Status, Total (12 行)
# 订单明细表(从表): OrderNo, LineNo, ProdName, Qty, Price, Amount (每单 2~5 行)
# 从表通过 OrderNo 与主表联动: 主表翻到哪个订单, 从表就只显示该订单的明细行.
#
# Delphi 版里主表的 Total 是在建明细表时回写的, 所以这里把主/从表放在一个函数里一起建.
# 返回 (主表, 从表)
# ---------------------------------------------------------------------------
def build_order_item_ds(owner):
statuses = ['已完成', '待发货', '已取消']
prods = ['无线鼠标', '机械键盘', '移动硬盘', '显示器', '耳机', '打印纸', '保温杯', '收纳盒']
orders = []
for i in range(12):
orders.append({
'OrderNo': 2026001 + i,
'CustNo': 1000 + (i * 3) % 20, # 引用客户表里的客户号
'OrderDate': (2026, 1 + i % 9, 1 + (i * 5) % 27),
'Status': statuses[i % 3],
'Total': 0.0, # 稍后由明细金额汇总得到
})
items = []
# 随机序列紧接着客户表之后继续使用(不重新设种子), 与 Delphi 版一致
for order in orders:
total = 0.0
n = 2 + delphi_random(4) # 每单 2~5 行明细
for j in range(1, n + 1):
prodname = prods[delphi_random(8)]
qty = delphi_random(20) + 1
price = float(delphi_random(190) + 10)
amount = qty * price
total += amount
items.append({'OrderNo': order['OrderNo'], 'LineNo': j, 'ProdName': prodname,
'Qty': qty, 'Price': price, 'Amount': amount})
order['Total'] = total
cds_order = make_cds(owner, 'cdsOrder', [
('OrderNo', 'I', 0), ('CustNo', 'I', 0), ('OrderDate', 'D', 0),
('Status', 'S', 10), ('Total', 'F', 0)], orders)
cds_order.IndexFieldNames = 'OrderNo'
cds_order.First()
cds_item = make_cds(owner, 'cdsOrderItem', [
('OrderNo', 'I', 0), ('LineNo', 'I', 0), ('ProdName', 'S', 30),
('Qty', 'I', 0), ('Price', 'F', 0), ('Amount', 'F', 0)], items)
# 建立主从联动. 要点: 从表的索引必须以联动字段(OrderNo)开头, MasterSource 指向主表的 DataSource,
# MasterFields 指定用主表的哪个字段去匹配从表的同名索引字段.
src = keep(DataSource(owner))
src.DataSet = cds_order
cds_item.IndexFieldNames = 'OrderNo;LineNo'
cds_item.MasterSource = src
cds_item.MasterFields = 'OrderNo'
cds_item.First()
return cds_order, cds_item
# ---------------------------------------------------------------------------
# 商品表(分组演示): Category, ProdNo, ProdName, Price, Qty, Amount (3 类 x 6 个 = 18 行)
# 数据按 Category 排序, 分组报表要求数据已按分组字段排好序.
# ---------------------------------------------------------------------------
def build_product_ds(owner):
cats = ['办公用品', '电子产品', '日用百货']
prods = [
['中性笔', '笔记本', '文件夹', '订书机', '打印纸', '便利贴'],
['无线鼠标', '机械键盘', '移动硬盘', '显示器', '耳机', '充电器'],
['保温杯', '毛巾', '洗衣液', '纸巾', '雨伞', '收纳盒'],
]
rows = []
for c in range(3):
for p in range(6):
price = float(delphi_random(300) + 5)
qty = delphi_random(100) + 1
rows.append({'Category': cats[c], 'ProdNo': (c + 1) * 100 + p + 1,
'ProdName': prods[c][p], 'Price': price, 'Qty': qty,
'Amount': price * qty})
ds = make_cds(owner, 'cdsProduct', [
('Category', 'S', 20), ('ProdNo', 'I', 0), ('ProdName', 'S', 30),
('Price', 'F', 0), ('Qty', 'I', 0), ('Amount', 'F', 0)], rows)
ds.IndexFieldNames = 'Category;ProdNo'
ds.First()
return ds
数据模块: uSampleData.py¶
# -*- coding: utf-8 -*-
"""样例模板用的模拟数据.
对应 Delphi 工程的 uSampleData.pas. Templates/Samples 里的 6 个 .rmf 是真实项目里做出来的报表模板,
它们按固定的数据集名称(rmds1/2/4/5/6/7)和字段名取数, 所以这里生成"同名同字段"的模拟数据,
模板不需要修改就能显示. 数据集的生成方式见 uDemoData.py 开头的说明.
数据集与模板的对应关系
rmds1 入库明细 inware.rmf (样例1)
rmds2 出库明细 outware.rmf (样例2)
rmds7 护理事故记录 accident.rmf (样例3)
rmds6 现金流水 cash.rmf (样例4)
rmds5 人员详情 elderdetail.rmf (样例5, 带照片)
rmds4 费用表 elderfeetable.rmf (样例6, 网格报表页)
(没有 rmds3: 只有未在样例页使用的 stock_d.rmf 会用到它, Delphi 原版同样没有提供.)
每个 build_xxx 函数开头都会重设随机种子, 并严格按 Delphi 版语句顺序调用 delphi_random,
所以生成的数据与 Delphi 版逐值一致.
"""
import os
import struct
from uDemoData import make_cds, set_rand_seed, delphi_random
# ---- 各数据集共用的取值表 ----
WHSE = ['一号仓', '二号仓']
CATS = ['办公用品', '电子产品', '日用百货']
MATS = [ # 物品名称, 按类别分组: MATS[类别][0..3]
['中性笔', '笔记本', '文件夹', '打印纸'],
['无线鼠标', '机械键盘', '移动硬盘', '耳机'],
['保温杯', '毛巾', '洗衣液', '收纳盒'],
]
SPECS = ['标准', '大号', '小号']
UNITS = ['个', '盒', '件']
PERSONS = ['张伟', '王芳', '李娜', '刘洋', '陈静', '杨帆', '赵磊', '孙悦']
DEPTS = ['行政部', '财务部', '销售部', '技术部']
def pick(arr, i):
"""循环取值, 等价于 Delphi 版的 Pick(A, I) = A[I mod 元素个数]"""
return arr[i % len(arr)]
def dstr(m, d):
"""生成 '2026-月-日' 形式的日期字符串. 月份故意不补零, 因为样例模板里用
Copy(日期,7,1)='-' 来判断月份是一位还是两位."""
return '2026-%d-%d' % (m, d)
def _fields(defs):
"""把 'whse_id:S' 这样的简写转成 make_cds 需要的字段定义.
:S 字符串(宽度 100), :F 浮点, :I 整数."""
result = []
for d in defs:
name, kind = d.split(':')
result.append((name, kind, 100 if kind == 'S' else 0))
return result
def make_photo_file(path):
"""生成 90x120 的纯色占位照片(24 位 BMP). 文件已存在则不动. 返回是否成功.
Templates/Samples 里已经带了 photo.bmp, 这个函数只在它被误删时兜底."""
if os.path.isfile(path):
return True
try:
os.makedirs(os.path.dirname(path), exist_ok=True)
w, h = 90, 120
stride = (w * 3 + 3) // 4 * 4 # BMP 每行字节数要对齐到 4 的倍数
pixel = bytes([0xE0, 0xC8, 0xA0]) # BGR 顺序, 即 Delphi 的 $00E0C8A0
row = pixel * w + b'\x00' * (stride - w * 3)
data = row * h
header = (b'BM' + struct.pack('<IHHI', 54 + len(data), 0, 0, 54) + # 文件头
struct.pack('<IiiHHIIiiII', 40, w, h, 1, 24, 0, len(data), 2835, 2835, 0, 0)) # 信息头
with open(path, 'wb') as f:
f.write(header + data)
return True
except Exception:
return False
# ---------------------------------------------------------------------------
# rmds1: 入库明细 (inware.rmf) 36 行, 按 仓库 + 日期 排序
# 注意 sum 字段 = cnt * price: 模板里的汇总原本写成 Sum(cnt*price), 补丁后改为 Sum(sum), 结果相同.
# ---------------------------------------------------------------------------
def build_inware_ds(owner):
defs = ['whse_id:S', 'inware_date:S', 'mat_name:S', 'cat_id:S', 'buyer:S', 'spec:S',
'unit:S', 'cnt:F', 'sum:F', 'check_id:S', 'price:F']
set_rand_seed(777)
rows = []
for i in range(36):
c = i % 3 # 类别下标
cnt = 1 + delphi_random(50)
price = 5 + delphi_random(200)
rows.append({ # 字典里各项的求值顺序就是随机数的调用顺序
'whse_id': pick(WHSE, i // 2),
'inware_date': dstr(1 + delphi_random(4), 1 + delphi_random(28)),
'mat_name': MATS[c][delphi_random(4)],
'cat_id': CATS[c],
'buyer': pick(PERSONS, i),
'spec': pick(SPECS, i),
'unit': pick(UNITS, i),
'cnt': cnt,
'sum': cnt * price,
'check_id': pick(PERSONS, i + 3),
'price': price,
})
ds = make_cds(owner, 'cdsInware', _fields(defs), rows)
ds.IndexFieldNames = 'whse_id;inware_date' # 模板按仓库、月份两级分组, 数据必须先排好序
ds.First()
return ds
# ---------------------------------------------------------------------------
# rmds2: 出库明细 (outware.rmf) 36 行, 按 仓库 + 日期 排序
# ---------------------------------------------------------------------------
def build_outware_ds(owner):
defs = ['whse_id:S', 'out_date:S', 'cat_id:S', 'mat_name:S', 'receiptor:S', 'spec:S',
'unit:S', 'cnt:F', 'dept:S', 'audit_name:S', 'input_date:S']
set_rand_seed(888)
rows = []
for i in range(36):
c = i % 3
rows.append({
'whse_id': pick(WHSE, i // 2),
'out_date': dstr(1 + delphi_random(4), 1 + delphi_random(28)),
'cat_id': CATS[c],
'mat_name': MATS[c][delphi_random(4)],
'receiptor': pick(PERSONS, i),
'spec': pick(SPECS, i),
'unit': pick(UNITS, i),
'cnt': 1 + delphi_random(30),
'dept': pick(DEPTS, i),
'audit_name': pick(PERSONS, i + 5),
'input_date': dstr(1 + delphi_random(4), 1 + delphi_random(28)),
})
ds = make_cds(owner, 'cdsOutware', _fields(defs), rows)
ds.IndexFieldNames = 'whse_id;out_date'
ds.First()
return ds
# ---------------------------------------------------------------------------
# rmds7: 护理事故记录 (accident.rmf) 20 行, 按录入日期排序
# ---------------------------------------------------------------------------
def build_accident_ds(owner):
health = ['跌倒', '发烧', '擦伤', '头晕']
memo = ['已处理, 观察24小时', '已联系家属并送医', '已上药包扎', '休息后好转']
defs = ['input_date:S', 'elder_name:S', 'con_code:S', 'duty_man:S', 'tend_date:S',
'healthmemoname:S', 'tend_memo:S']
set_rand_seed(999)
rows = []
for i in range(20):
rows.append({
'input_date': dstr(1 + i % 4, 1 + delphi_random(28)),
'elder_name': pick(PERSONS, i),
'con_code': 'C%04d' % (1000 + i),
'duty_man': pick(PERSONS, i + 2),
'tend_date': dstr(1 + i % 4, 1 + delphi_random(28)),
'healthmemoname': pick(health, i),
'tend_memo': pick(memo, i + 1),
})
ds = make_cds(owner, 'cdsAccident', _fields(defs), rows)
ds.IndexFieldNames = 'input_date'
ds.First()
return ds
# ---------------------------------------------------------------------------
# rmds6: 现金/银行流水 (cash.rmf) 20 行, 按 id 排序
#
# Delphi 原版数据里还有一个 id=21 的余额汇总行, 由模板脚本 GetValue('rmds6."id"')=21 把它隐藏.
# 测试环境里这段脚本无法编译(见 uMain.PatchScript), 程序会把它清空, 所以这里干脆不生成该行.
# 显示效果与原来相同: 被隐藏的行本来就不显示, 且它的借贷字段为空, 不影响合计.
# ---------------------------------------------------------------------------
def build_cash_ds(owner):
proj = ['办公费', '差旅费', '材料费', '水电费']
pays = ['现金', '银行转账', '支票']
defs = ['id:I', 'c_blance:F', 'b_blance:F', 'in_date:S', 'name:S', 'project:S', 'pay:S',
'remark:S', 'c_debit:F', 'c_credit:F', 'b_debit:F', 'b_credit:F']
set_rand_seed(555)
rows = []
for i in range(1, 21):
rows.append({ # c_blance / b_blance(余额)留空, 即 NULL
'id': i,
'in_date': dstr(1 + i % 4, 1 + delphi_random(28)),
'name': pick(PERSONS, i),
'project': pick(proj, i),
'pay': pick(pays, i),
'remark': '演示数据%d' % i,
'c_debit': float(delphi_random(3000)),
'c_credit': float(delphi_random(2000)),
'b_debit': float(delphi_random(5000)),
'b_credit': float(delphi_random(4000)),
})
ds = make_cds(owner, 'cdsCash', _fields(defs), rows)
ds.IndexFieldNames = 'id'
ds.First()
return ds
# ---------------------------------------------------------------------------
# rmds5: 人员详情 (elderdetail.rmf) 15 行, 按 instate(入住状态) 排序, 模板按它分组
#
# fphoto 是照片文件路径, 模板里的图片对象用它显示照片(见 uMain.FixPictures).
# 末尾三个 *_name 字段是 Delphi 版没有的: 模板里原来用 IF([instate]=0,'试住老人信息',...) 这样的
# 数值比较表达式把代码翻译成文字, 程序加载前会把这些表达式替换成直接读下面预先算好的文字字段
# (见 uMain.FILE_PATCH_RULES). 文字内容与模板里的 IF 逻辑逐项一致.
# ---------------------------------------------------------------------------
def build_elder_detail_ds(owner, photo):
nation = ['汉族', '回族', '满族']
place = ['北京', '上海', '广东', '四川']
fee = ['自费', '医保', '公费']
level = ['一级', '二级', '三级']
defs = ['fphoto:S', 'instate:I', 'oldername:S', 'gender:I', 'birth_date:S', 'nation:S',
'nativeplace:S', 'telephone:S', 'medicare:S', 'fee_type:S', 'marrige:I',
'month_fee:F', 'care_people:S', 'care_level:S',
'instate_name:S', 'gender_name:S', 'marrige_name:S']
instate_names = ['试住老人信息', '入住老人信息', '退住老人信息', '转院老人信息'] # instate 0~3, 其它为"死亡"
marrige_names = ['离婚', '已婚', '离异'] # marrige 0~2, 其它为"喪偶"
set_rand_seed(444)
rows = []
for i in range(15):
rows.append({
'fphoto': photo,
'instate': i % 5,
'oldername': pick(PERSONS, i),
'gender': i % 2, # 0 男, 1 女
'birth_date': '%d-%d-%d' % (1935 + delphi_random(15), 1 + delphi_random(12),
1 + delphi_random(28)),
'nation': pick(nation, i),
'nativeplace': pick(place, i),
'telephone': '1390%07d' % delphi_random(10000000),
'medicare': pick(fee, i + 1),
'fee_type': pick(fee, i),
'marrige': i % 4, # 必须是整数, 模板里按数字 0..3 判断
'month_fee': float(2000 + delphi_random(30) * 100),
'care_people': pick(PERSONS, i + 4),
'care_level': pick(level, i),
'instate_name': instate_names[i % 5] if i % 5 < 4 else '死亡',
'gender_name': '男' if i % 2 == 0 else '女',
'marrige_name': marrige_names[i % 4] if i % 4 < 3 else '喪偶',
})
ds = make_cds(owner, 'cdsElderDetail', _fields(defs), rows)
ds.IndexFieldNames = 'instate'
ds.First()
return ds
# ---------------------------------------------------------------------------
# rmds4: 费用表 (elderfeetable.rmf, 网格报表页) 15 行
# 每人 5 个费用项目 (record0..4 / fee0..4), allfee 是合计.
# ---------------------------------------------------------------------------
def build_elder_fee_ds(owner):
rec = ['床位费', '护理费', '餐费', '医疗费', '其他']
defs = ['elder_name:S', 'con_code:S', 'record0:S', 'fee0:F', 'mindate0:S', 'maxdate0:S',
'record1:S', 'fee1:F', 'record2:S', 'fee2:F', 'record3:S', 'fee3:F',
'record4:S', 'fee4:F', 'allfee:F']
set_rand_seed(333)
rows = []
for i in range(15):
r = {
'elder_name': pick(PERSONS, i),
'con_code': 'C%04d' % (1000 + i),
'mindate0': dstr(1, 1 + delphi_random(10)),
'maxdate0': dstr(4, 15 + delphi_random(10)),
}
total = 0.0
for k in range(5):
r['record%d' % k] = rec[k]
r['fee%d' % k] = float(100 + delphi_random(90) * 10)
total += r['fee%d' % k]
r['allfee'] = total
rows.append(r)
ds = make_cds(owner, 'cdsElderFee', _fields(defs), rows)
ds.First() # 该数据集没有排序要求
return ds
设计文件: uMain.sfm¶
object frmMain: TForm
Left = 0
Top = 0
Caption = 'ReportMachine '#22810#21151#33021#28436#31034
ClientHeight = 520
ClientWidth = 973
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -12
Font.Name = 'Segoe UI'
Font.Style = []
Position = poScreenCenter
TextHeight = 15
object memLog: TMemo
Left = 0
Top = 390
Width = 973
Height = 130
Align = alBottom
ReadOnly = True
ScrollBars = ssVertical
TabOrder = 0
end
object memHelp: TMemo
Left = 523
Top = 0
Width = 450
Height = 390
Align = alRight
ReadOnly = True
ScrollBars = ssVertical
TabOrder = 1
end
object pnlClient: TPanel
Left = 0
Top = 0
Width = 523
Height = 390
Align = alClient
BevelOuter = bvNone
TabOrder = 2
object pcMain: TPageControl
Left = 0
Top = 0
Width = 523
Height = 390
ActivePage = tsBuiltin
Align = alClient
TabOrder = 0
object tsBuiltin: TTabSheet
Caption = #20869#32622#28436#31034
object lblDemo1: TLabel
Left = 12
Top = 19
Width = 116
Height = 15
Caption = #28436#31034'1 '#23458#25143#28165#21333'('#21333#34920')'
end
object lblDemo2: TLabel
Left = 12
Top = 63
Width = 108
Height = 15
Caption = #28436#31034'2 '#35746#21333#20027#20174#26126#32454
end
object lblDemo3: TLabel
Left = 12
Top = 107
Width = 96
Height = 15
Caption = #28436#31034'3 '#21464#37327#19982#36807#28388
end
object lblCity: TLabel
Left = 12
Top = 151
Width = 113
Height = 15
Caption = ' '#36807#28388#22478#24066'('#28436#31034'3):'
end
object lblDemo4: TLabel
Left = 12
Top = 195
Width = 108
Height = 15
Caption = #28436#31034'4 '#21830#21697#20998#32452#27719#24635
end
object lblDemo5: TLabel
Left = 12
Top = 249
Width = 129
Height = 15
Caption = #28436#31034'5 '#39044#29983#25104#25253#34920'(.rmp)'
end
object lblDemo6: TLabel
Left = 12
Top = 293
Width = 60
Height = 15
Caption = #28436#31034'6 '#23548#20986
end
object lblTplDir: TLabel
Left = 12
Top = 337
Width = 48
Height = 15
Caption = #27169#26495#30446#24405
end
object btnDemo1Design: TButton
Tag = 10
Left = 260
Top = 14
Width = 64
Height = 26
Caption = #35774#35745
TabOrder = 0
end
object btnDemo1Preview: TButton
Tag = 11
Left = 330
Top = 14
Width = 64
Height = 26
Caption = #39044#35272
TabOrder = 1
end
object btnDemo1Print: TButton
Tag = 12
Left = 400
Top = 14
Width = 64
Height = 26
Caption = #25171#21360
TabOrder = 2
end
object btnDemo2Design: TButton
Tag = 20
Left = 260
Top = 58
Width = 64
Height = 26
Caption = #35774#35745
TabOrder = 3
end
object btnDemo2Preview: TButton
Tag = 21
Left = 330
Top = 58
Width = 64
Height = 26
Caption = #39044#35272
TabOrder = 4
end
object btnDemo2Print: TButton
Tag = 22
Left = 400
Top = 58
Width = 64
Height = 26
Caption = #25171#21360
TabOrder = 5
end
object btnDemo3Design: TButton
Tag = 30
Left = 260
Top = 102
Width = 64
Height = 26
Caption = #35774#35745
TabOrder = 6
end
object btnDemo3Preview: TButton
Tag = 31
Left = 330
Top = 102
Width = 64
Height = 26
Caption = #39044#35272
TabOrder = 7
end
object btnDemo3Print: TButton
Tag = 32
Left = 400
Top = 102
Width = 64
Height = 26
Caption = #25171#21360
TabOrder = 8
end
object cbCity: TComboBox
Left = 260
Top = 147
Width = 134
Height = 23
Style = csDropDownList
ItemIndex = 0
TabOrder = 9
Text = #20840#37096#22478#24066
Items.Strings = (
#20840#37096#22478#24066
#21271#20140
#19978#28023
#24191#24030
#28145#22323)
end
object btnDemo4Design: TButton
Tag = 40
Left = 260
Top = 190
Width = 64
Height = 26
Caption = #35774#35745
TabOrder = 10
end
object btnDemo4Preview: TButton
Tag = 41
Left = 330
Top = 190
Width = 64
Height = 26
Caption = #39044#35272
TabOrder = 11
end
object btnDemo4Print: TButton
Tag = 42
Left = 400
Top = 190
Width = 64
Height = 26
Caption = #25171#21360
TabOrder = 12
end
object btnPrepSave: TButton
Tag = 50
Left = 260
Top = 244
Width = 64
Height = 26
Caption = #29983#25104#20445#23384
TabOrder = 13
end
object btnPrepLoad: TButton
Tag = 51
Left = 330
Top = 244
Width = 64
Height = 26
Caption = #36733#20837#39044#35272
TabOrder = 14
end
object btnExpHtml: TButton
Tag = 60
Left = 260
Top = 288
Width = 64
Height = 26
Caption = 'HTML'
TabOrder = 15
end
object btnExpXls: TButton
Tag = 61
Left = 330
Top = 288
Width = 64
Height = 26
Caption = 'Excel'
TabOrder = 16
end
object btnOpenDir: TButton
Tag = 70
Left = 260
Top = 332
Width = 64
Height = 26
Caption = #25171#24320
TabOrder = 17
end
end
object tsSamples: TTabSheet
Caption = #26679#20363#27169#26495
object lblSample1: TLabel
Left = 12
Top = 19
Width = 181
Height = 15
Caption = #26679#20363'1 inware '#20837#24211#26126#32454'('#22810#32423#20998#32452')'
end
object lblSample2: TLabel
Left = 12
Top = 57
Width = 186
Height = 15
Caption = #26679#20363'2 outware '#20986#24211#26126#32454'('#22810#32423#20998#32452')'
end
object lblSample3: TLabel
Left = 12
Top = 95
Width = 185
Height = 15
Caption = #26679#20363'3 accident '#35760#24405#28165#21333'('#25353#26376#20998#32452')'
end
object lblSample4: TLabel
Left = 12
Top = 133
Width = 176
Height = 15
Caption = #26679#20363'4 cash '#27969#27700#36134'('#24102#25253#34920#33050#26412')'
end
object lblSample5: TLabel
Left = 12
Top = 171
Width = 196
Height = 15
Caption = #26679#20363'5 elderdetail '#35814#24773'('#22270#29255'+'#33050#26412'+IF)'
end
object lblSample6: TLabel
Left = 12
Top = 209
Width = 184
Height = 15
Caption = #26679#20363'6 elderfeetable '#36153#29992#34920'('#32593#26684#39029')'
end
object btnSample1Preview: TButton
Tag = 100
Left = 270
Top = 14
Width = 64
Height = 26
Caption = #39044#35272
TabOrder = 0
end
object btnSample1Design: TButton
Tag = 101
Left = 340
Top = 14
Width = 64
Height = 26
Caption = #35774#35745
TabOrder = 1
end
object btnSample1Print: TButton
Tag = 102
Left = 410
Top = 14
Width = 64
Height = 26
Caption = #25171#21360
TabOrder = 2
end
object btnSample2Preview: TButton
Tag = 110
Left = 270
Top = 52
Width = 64
Height = 26
Caption = #39044#35272
TabOrder = 3
end
object btnSample2Design: TButton
Tag = 111
Left = 340
Top = 52
Width = 64
Height = 26
Caption = #35774#35745
TabOrder = 4
end
object btnSample2Print: TButton
Tag = 112
Left = 410
Top = 52
Width = 64
Height = 26
Caption = #25171#21360
TabOrder = 5
end
object btnSample3Preview: TButton
Tag = 120
Left = 270
Top = 90
Width = 64
Height = 26
Caption = #39044#35272
TabOrder = 6
end
object btnSample3Design: TButton
Tag = 121
Left = 340
Top = 90
Width = 64
Height = 26
Caption = #35774#35745
TabOrder = 7
end
object btnSample3Print: TButton
Tag = 122
Left = 410
Top = 90
Width = 64
Height = 26
Caption = #25171#21360
TabOrder = 8
end
object btnSample4Preview: TButton
Tag = 130
Left = 270
Top = 128
Width = 64
Height = 26
Caption = #39044#35272
TabOrder = 9
end
object btnSample4Design: TButton
Tag = 131
Left = 340
Top = 128
Width = 64
Height = 26
Caption = #35774#35745
TabOrder = 10
end
object btnSample4Print: TButton
Tag = 132
Left = 410
Top = 128
Width = 64
Height = 26
Caption = #25171#21360
TabOrder = 11
end
object btnSample5Preview: TButton
Tag = 140
Left = 270
Top = 166
Width = 64
Height = 26
Caption = #39044#35272
TabOrder = 12
end
object btnSample5Design: TButton
Tag = 141
Left = 340
Top = 166
Width = 64
Height = 26
Caption = #35774#35745
TabOrder = 13
end
object btnSample5Print: TButton
Tag = 142
Left = 410
Top = 166
Width = 64
Height = 26
Caption = #25171#21360
TabOrder = 14
end
object btnSample6Preview: TButton
Tag = 150
Left = 270
Top = 204
Width = 64
Height = 26
Caption = #39044#35272
TabOrder = 15
end
object btnSample6Design: TButton
Tag = 151
Left = 340
Top = 204
Width = 64
Height = 26
Caption = #35774#35745
TabOrder = 16
end
object btnSample6Print: TButton
Tag = 152
Left = 410
Top = 204
Width = 64
Height = 26
Caption = #25171#21360
TabOrder = 17
end
object chkTitle: TCheckBox
Left = 12
Top = 292
Width = 330
Height = 17
Caption = #39044#35272'/'#25171#21360#21069', '#29992#20195#30721#25913#20889#26631#39064#25991#26412#26694' Memo1:'
TabOrder = 18
end
object edtTitle: TEdit
Left = 12
Top = 314
Width = 300
Height = 23
TabOrder = 19
Text = #30001#31243#24207#36816#34892#26102#25913#20889#30340#26631#39064
end
end
object tsAdvanced: TTabSheet
Caption = #39640#32423#21151#33021
object lblOpenAny: TLabel
Left = 172
Top = 19
Width = 302
Height = 15
Caption = #36873#25321#20219#24847#27169#26495#25991#20214', '#39044#35272#25110#35774#35745'('#21487#29992#26469#27979#35797#20320#33258#24049#30340#27169#26495')'
end
object lblComposite: TLabel
Left = 172
Top = 63
Width = 179
Height = 15
Caption = #28436#31034'1+'#28436#31034'2 '#21512#24182#21040#19968#20010#39044#35272#31383#21475
end
object lblDatasets: TLabel
Left = 12
Top = 110
Width = 490
Height = 60
AutoSize = False
Caption =
#25968#25454#38598#21517#31216'('#35774#35745#22120#37324#36873#29992'):'#13#10' rmCustomer rmOrder rmOrderItem rmProduct'#13#10' ' +
' rmds1 rmds2 rmds4 rmds5 rmds6 rmds7 '
end
object btnOpenAny: TButton
Tag = 210
Left = 12
Top = 14
Width = 150
Height = 26
Caption = #25171#24320#20219#24847' .rmf ...'
TabOrder = 0
end
object btnComposite: TButton
Tag = 220
Left = 12
Top = 58
Width = 150
Height = 26
Caption = #22810#25253#34920#21512#24182#39044#35272
TabOrder = 1
end
end
end
end
end
详细分析¶
功能概述¶
本示例是 glcl ReportMachine 报表组件的综合演示,分为三个页面:
- 页1「内置演示」:4 个需要用户在设计器中自行设计并保存的报表 + 预生成报表(.rmp)+ 导出(HTML / Excel)。
- 页2「样例模板」:6 个真实使用中的
.rmf模板可直接加载(入库单、出库单、事故登记、现金日记账、老人明细、老人费用表)。 - 页3「高级功能」:打开任意
.rmf、把多个报表合并预览。
所有数据均由内存 TClientDataSet 生成,无需数据库。
使用的组件¶
- frmMain (TForm) — 主窗体
- PageControl / TabSheet — 三个标签页
- Memo (memLog / memHelp) — 日志框 / 帮助面板
- Button ×37 — 所有按钮共用
BtnClick,用Tag区分动作 - ComboBox (cbCity) — 演示3 的城市过滤选择
- CheckBox / Edit — 「用代码改写标题」选项与标题内容
- RMReport — 报表核心对象(每次操作动态创建)
- RMDesigner — 报表设计器(
TemplateDir决定打开/保存对话框的默认目录) - RMCompositeReport — 多报表合并预览
- RMDBDataSet — 把 ClientDataSet 包装成报表可用数据集
- RMHTMExport / RMXLSExport — 导出为 HTML / Excel
技术特点¶
- 数据集与模板解耦:程序用
build_customer_ds/build_order_item_ds/build_product_ds等函数生成内存数据,再通过MakeRmDS()包一层RMDBDataSet,其Name就是模板里看到的[rmCustomer."CustName"]这样的数据集名。样例模板固定使用rmds1、rmds2、rmds4、rmds5、rmds6、rmds7(没有 rmds3)。 - 主从报表:演示2 通过 ClientDataSet 的
MasterSource/MasterFields(OrderNo)建立主从关系,报表里无需再配置连接。 - 报表变量(OnGetValue):演示3 通过
self.FReport.OnGetValue = self.ReportGetValue提供[ReportTitle]、[Company]、[PrintUser]、[PrintTime]、[FilterInfo]、[RecCount]、[GlobalNote]等变量。重要经验:Python 回调OnGetValue会干扰报表取数,曾导致样例1、样例5 的数据单元格全部变成空白,因此只有真正需要变量的演示3 才挂这个回调。 - 预生成报表(.rmp):演示5 先用
PrepareReport()生成结果页并SavePreparedReport()保存为demo1_prepared.rmp,之后用LoadPreparedReport()载入预览,无需数据集,适合归档。 - 样例模板修补:两个开关
ENABLE_TEMPLATE_PATCH(对inware.rmf、elderdetail.rmf做“等长替换”,去掉模板里的数值运算表达式,生成临时副本,原模板不改)与ENABLE_SCRIPT_PATCH(清空脚本里调用GetValue/GetFieldValue的过程,避免 “Undeclared Identifier”)。修补只用于样例的预览/打印,设计模式必须打开原模板,否则保存时会把修补后的内容写回样例文件。 - 按钮 Tag 编码:
10~42内置演示(十位=演示编号,个位 0 设计/1 预览/2 打印);50/51预生成报表保存/载入;60/61导出 HTML/Excel;70打开模板目录;100~152样例模板;210打开任意 rmf;220多报表合并。 - 异常保护:
BtnClick用try/except包裹,任何异常都写进日志并弹框,不让程序崩溃。
窗体属性¶
- 主窗体包含
PageControl三个页签。 - 页1 内置演示:
lblDemo1~4、每组btnDesign/Preview/Print、cbCity过滤下拉、btnPrepSave/btnPrepLoad、btnExpHtml/btnExpXls、btnOpenDir。 - 页2 样例模板:
lblSample1~6、每组btnPreview/Design/Print、chkTitle、edtTitle。 - 页3 高级功能:
btnOpenAny(打开任意 rmf)、btnComposite(多报表合并)、lblDatasets。 - 左下为
memLog(日志,带时间戳),右侧为memHelp(帮助)。
代码分析¶
导入的模块:
- os、re、time
- from glcl import *
- uDemoData、uSampleData
定义的类:
- frmMain
主要方法:
- BuildData / MakeRmDS — 生成数据集并包装为 RM 数据集
- NewReport / RunFile — 创建报表并执行预览/设计/打印
- DoTpl / DoSample / DoOpenAny / DoComposite / DoSavePrepared / DoLoadPrepared / DoExport — 各按钮动作
- PatchedTemplate / PatchScript / FixPictures — 样例模板修补
- BtnClick — 统一按钮事件(按 Tag 分发)
- ReportGetValue — 报表变量回调(仅演示3 使用)
- Log / ShowHelp / Confirm / quoted_str — 辅助方法
关键 API:
- RMReport.LoadFromFile / SaveToFile / PrepareReport / ShowReport / DesignReport / PrintReport / ExportTo / SavePreparedReport / LoadPreparedReport / ShowPreparedReport / FindObject
- RMDesigner.TemplateDir
- RMCompositeReport.Reports / ShowReport
- RMDBDataSet.DataSet
事件绑定:
- 所有按钮通过 getattr(self, name).OnClick = self.BtnClick 统一绑定,由 Sender.Tag 区分