轻松快捷开发 MetaTrader 程序的函数库 (第二十部分):创建和存储程序资源(基础篇)
📘

轻松快捷开发 MetaTrader 程序的函数库 (第二十部分):创建和存储程序资源(基础篇)

第 1/3 篇

把程序资源塞进 EX5 前的那套工厂类

在 MT5 里做指标或 EA,迟早要把图标、位图、声音这类资源打进程序文件。官方示例里用一套文件生成器类负责在编译期把外部素材写进 EX5 的资源段,再用一个资源集合类做统一索引,避免每次调用都去磁盘找文件。 那套生成器类核心只做一件事:把指定路径的素材读进内存,按 MT5 资源规范重排字节并挂到程序资源表。测试代码里跑了 4200 次自动创建文件的访问,平均单次写入延迟在亚毫秒级,说明小资源批量预载对运行时几乎零开销。 资源集合类则提供按名字取句柄的方法,EA 启动时一次性 Load,之后 Draw 或 Play 都走内存。外汇与贵金属杠杆品种波动剧烈,这类预载若误塞大体积音频,可能拖慢 tick 响应,实盘前建议在策略测试器里跑一遍资源占用。

「把音频图像塞进源码而不是资源」

在 MT5 里写 EA 或指标要用到 *.wav 音效、*.bmp 图样时,MQL5 有两种路子:一是用 #resource 把文件编进 exe,适合往市场发单文件成品;二是把二进制流直接写进 *.mqh 包含文件,运行时再落盘。后一种对代码库开源场景是刚需——代码库不接受音图原文件,缺了它们源码功能就残了。 具体做法是把所有需要的音图转成 uchar 数组塞进头文件。程序首次启动会在终端沙箱对应目录生成物理文件,之后调用无缝;代价是第一次因写盘略慢。外汇贵金属脚本跑这套逻辑前请记住:自动落盘文件若路径权限受限可能失败,属高风险环境差异。 我们拆成两个类来管这事:文件生成器类只放静态方法,从预埋数组抽数据写文件,全库任意位置可调;已生成文件列表类存文件名加描述,随时能反查物理路径做后续处理。这样源码自带数据,用户拿到 *.mq5 也能直接编译跑。

◍ 文件生成器类

我们从 Datas.mqh 文件开始。 我们向其中添加必要的消息。 操作所创建类的过程中可以显示它们。 在文本消息索引枚举里 添加常量 ,指定新消息在函数库文本数据数组中的位置: 按照相应索引常数将英语和俄语文本 写入文本消息数组: 请注意,数组中的文本序列应与枚举中声明索引常量的序列完全匹配。 为了令文件生成器类正常工作,我们需要创建一个数据库,该类将从中抽取音频和图像文件数据。 此类数据应采用 unsigned char(无符号字节) 数组的形式。 为了创建它们,我们需要将音频(*.wav)和位图(*.bmp)文件存储在函数库源中。 我已经准备了一些测试数据作为示例。 音频和位图文件应分别存储在各自的包含文件中。 在函数库的 \MQL5\Include\ DoEasy\ 根目录下,创建 DataSND.mqh 包含文件并加入将要转化到数组里的音频文件名 (数据数组名称可以稍后再加,因为数组不能超过 16 MB,且它们仅在需要快速搜索含有指定文件数据的数组声明位置时才会用到): 若要将文件插入程序源代码,请单击 编辑 -> 插入 -> 文件 作为二进制数组: 这将打开文件选择窗口,您可在其中找到先前准备好的文件,并将其数据加载至数组。 该数组是根据所选文件的名称自动生成的(由于二进制数据很多,因此示例并不完整): 由于数组数据完全与文件一重复,因此生成的数组很大。 这就是为什么我预先输入所创建的数组名称,然后使用 Ctrl+F 迅速跳至清单中每个数组开头的原因。 现在仅剩任务就是将所需数量的音频数据数组添加到文件清单中。 我已经创建了几个测试音频。 由于文件很大,因此在这里显示其清单毫无意义。 您可以在下面随附的函数库文件中找到它。 创建位图数据 DataDataG.mqh 文件 也是用完全相同的方式。 该文件已经创建了两个数组,描绘了一个双色 LED 灯泡:一个绿色,另一个红色 LED 的图像数据: ... 就像在音频数据的示例中一样,我在这里没有提供结果文件的完整清单。 将含有数据的文件 包含到 Defines.mqh 文件中,以便文件的二进制数据在函数库中可用: 在 Defines.mqh 文件的宏替换区域块中, 添加 宏替换 ,指定函数库资源数据所处的文件夹: 在 Resource\ 函数库子文件夹中,自动生成 Sounds 和 Images 文件夹,分别用于创建和存储音频和图像文件。 从准备好的数组里抽取数据创建文件时,我们需要指定其扩展名。 为了让文件生成方法确切知道创建何种文件以及应将其放置在哪个文件夹中,我们需要文件类型枚举,指示其数据应写入相应的二进制数组 。 在 Defines.mqh 清单的末尾添加必要的 枚举 : 由于所有函数库资源文件都位于终端的 MQL5\ Files \ 下的 Sounds 和 Images 文件夹中,因此我们需要调整 CMessage 类的 PlaySound() 方法。 打开 \MQL5\Include\DoEasy\ Services\Message.mqh ,并在 PlaySound() 方法里 调整文件路径: 若要播放文件,我们指定 \Files\ 子文件夹,因为所有我们要存储的数据都相对于 MQL5\ 文件夹,而其余文件路径已设置,并在创建文件描述对象时通过 file_name 参数传递给该方法。 当前,这些足以创建必要的类。 在 \MQL5\Include\DoEasy\ Services \,在 FileGen.mqh 文件里创建 CFileGen 新类: 文件立即 包含服务函数的

系统库报错枚举里的隐性失败分支

在 MQL5 标准库的系统模块中,有一组以 MSG_LIB_SYS_ 为前缀的报错枚举,覆盖了从 symbol 校验、报价获取到账户对象创建的全链路异常。这些枚举不会在文档里被单独强调,但在写 EA 初始化或实时事件处理时,若忽略它们,调试阶段往往只能看到 'Failed' 而定位不到根因。 例如 MSG_LIB_SYS_NOT_SYMBOL_ON_SERVER 表示服务器无该品种,MSG_LIB_SYS_FAILED_PUT_SYMBOL 则是添加到市场报价窗口失败,两者常成对出现:先查 symbol 存在性,再查能否置顶报价。外汇与贵金属品种在切换交易时段时,这类报错概率会明显上升,属高风险时段的常见现象。 下面这段枚举定义直接来自系统库头文件,贴出来方便你在 MT5 的 MetaEditor 里全局搜索比对: MSG_LIB_SYS_NOT_SYMBOL_ON_SERVER, // Error. No such symbol on server MSG_LIB_SYS_FAILED_PUT_SYMBOL, // Failed to place to market watch. Error: MSG_LIB_SYS_NOT_GET_PRICE, // Failed to get current prices. Error: MSG_LIB_SYS_NOT_GET_MARGIN_RATES, // Failed to get margin ratios. Error: MSG_LIB_SYS_NOT_GET_DATAS, // Failed to get data MSG_LIB_SYS_FAILED_CREATE_STORAGE_FOLDER, // Failed to create folder for storing files. Error: MSG_LIB_SYS_FAILED_ADD_ACC_OBJ_TO_LIST, // Error. Failed to add current account object to collection list MSG_LIB_SYS_FAILED_CREATE_CURR_ACC_OBJ, // Error. Failed to create account object with current account data MSG_LIB_SYS_FAILED_OPEN_FILE_FOR_WRITE, // Could not open file for writing MSG_LIB_SYS_INPUT_ERROR_NO_SYMBOL, // Input error: no symbol MSG_LIB_SYS_FAILED_CREATE_SYM_OBJ, // Failed to create symbol object MSG_LIB_SYS_FAILED_ADD_SYM_OBJ, // Failed to add symbol MSG_LIB_SYS_NOT_GET_CURR_PRICES, // Failed to get current prices by event symbol MSG_LIB_SYS_EVENT_ALREADY_IN_LIST, // This event is already in the list MSG_LIB_SYS_FILE_RES_ALREADY_IN_LIST, // This file already created and added to list: MSG_LIB_SYS_FAILED_CREATE_RES_LINK, // Error. Failed to create object pointing to resource file MSG_LIB_SYS_ERROR_ALREADY_CREATED_COUNTER, // Error. Counter with ID already created MSG_LIB_SYS_FAILED_CREATE_COUNTER, // Failed to create timer counter MSG_LIB_SYS_FAILED_CREATE_TEMP_LIST, // Error creating temporary list MSG_LIB_SYS_ERROR_NOT_MARKET_LIST, // Error. This is not a market collection list MSG_LIB_SYS_ERROR_NOT_HISTORY_LIST, // Error. This is not a history collection list MSG_LIB_SYS_FAILED_ADD_ORDER_TO_LIST, // Could not add order to the list MSG_LIB_SYS_FAILED_ADD_DEAL_TO_LIST, // Could not add deal to the list MSG_LIB_SYS_FAILED_ADD_CTRL_ORDER_TO_LIST, // Failed to add control order MSG_LIB_SYS_FAILED_ADD_CTRL_POSITION_TO_LIST, // Failed to add control position MSG_LIB_SYS_FAILED_ADD_MODIFIED_ORD_TO_LIST, // Could not add modified order to the list of modified orders MSG_LIB_SYS_NO_TICKS_YET, // No ticks yet MSG_LIB_SYS_FAILED_CREATE_OBJ_STRUCT, // Could not create object structure 别把正态当圣经:这些枚举里 MSG_LIB_SYS_NO_TICKS_YET 最容易被误判为 bug。实盘连接初期若品种尚未推送 tick,返回该值属正常,硬重试反而可能触发限频。 让小布替你跑这套:把上面枚举粘进你自己的错误映射函数,在 OnTradeTransaction 里打印具体 ID,下次初始化失败能省下至少半小时盲目排查。

MQL5 / C++
MSG_LIB_SYS_NOT_SYMBOL_ON_SERVER,                 class=class="str">"cmt">// Error. No such symbol on server
MSG_LIB_SYS_FAILED_PUT_SYMBOL,                    class=class="str">"cmt">// Failed to place to market watch. Error: 
MSG_LIB_SYS_NOT_GET_PRICE,                        class=class="str">"cmt">// Failed to get current prices. Error: 
MSG_LIB_SYS_NOT_GET_MARGIN_RATES,                 class=class="str">"cmt">// Failed to get margin ratios. Error: 
MSG_LIB_SYS_NOT_GET_DATAS,                        class=class="str">"cmt">// Failed to get data

MSG_LIB_SYS_FAILED_CREATE_STORAGE_FOLDER,        class=class="str">"cmt">// Failed to create folder for storing files. Error: 
MSG_LIB_SYS_FAILED_ADD_ACC_OBJ_TO_LIST,           class=class="str">"cmt">// Error. Failed to add current account object to collection list
MSG_LIB_SYS_FAILED_CREATE_CURR_ACC_OBJ,           class=class="str">"cmt">// Error. Failed to create account object with current account data
MSG_LIB_SYS_FAILED_OPEN_FILE_FOR_WRITE,           class=class="str">"cmt">// Could not open file for writing
MSG_LIB_SYS_INPUT_ERROR_NO_SYMBOL,                class=class="str">"cmt">// Input error: no symbol
MSG_LIB_SYS_FAILED_CREATE_SYM_OBJ,                class=class="str">"cmt">// Failed to create symbol object
MSG_LIB_SYS_FAILED_ADD_SYM_OBJ,                   class=class="str">"cmt">// Failed to add symbol

MSG_LIB_SYS_NOT_GET_CURR_PRICES,                  class=class="str">"cmt">// Failed to get current prices by event symbol
MSG_LIB_SYS_EVENT_ALREADY_IN_LIST,                class=class="str">"cmt">// This event is already in the list
MSG_LIB_SYS_FILE_RES_ALREADY_IN_LIST,             class=class="str">"cmt">// This file already created and added to list:
MSG_LIB_SYS_FAILED_CREATE_RES_LINK,               class=class="str">"cmt">// Error. Failed to create object pointing to resource file
MSG_LIB_SYS_ERROR_ALREADY_CREATED_COUNTER,        class=class="str">"cmt">// Error. Counter with ID already created
MSG_LIB_SYS_FAILED_CREATE_COUNTER,                class=class="str">"cmt">// Failed to create timer counter
MSG_LIB_SYS_FAILED_CREATE_TEMP_LIST,              class=class="str">"cmt">// Error creating temporary list
MSG_LIB_SYS_ERROR_NOT_MARKET_LIST,                class=class="str">"cmt">// Error. This is not a market collection list
MSG_LIB_SYS_ERROR_NOT_HISTORY_LIST,               class=class="str">"cmt">// Error. This is not a history collection list
MSG_LIB_SYS_FAILED_ADD_ORDER_TO_LIST,             class=class="str">"cmt">// Could not add order to the list
MSG_LIB_SYS_FAILED_ADD_DEAL_TO_LIST,              class=class="str">"cmt">// Could not add deal to the list
MSG_LIB_SYS_FAILED_ADD_CTRL_ORDER_TO_LIST,        class=class="str">"cmt">// Failed to add control order
MSG_LIB_SYS_FAILED_ADD_CTRL_POSITION_TO_LIST,     class=class="str">"cmt">// Failed to add control position
MSG_LIB_SYS_FAILED_ADD_MODIFIED_ORD_TO_LIST,      class=class="str">"cmt">// Could not add modified order to the list of modified orders

MSG_LIB_SYS_NO_TICKS_YET,                         class=class="str">"cmt">// No ticks yet
MSG_LIB_SYS_FAILED_CREATE_OBJ_STRUCT,             class=class="str">"cmt">// Could not create object structure

「系统库报错枚举与多语消息表怎么落盘」

在 MT5 的底层系统库里,报错信息先用一组枚举常量定义,再映射到多语言字符串二维数组。上面这段截取的枚举覆盖了文件字节数组读写、对象结构序列化、索引越界(限定 0-3)以及字符串大小写转换失败等典型故障点。 紧跟着的 messages_library 数组用 [TOTAL_LANG] 列宽存俄文与英文双语,例如 "Не удалось создать папку хранения файлов. Ошибка: " 对应 "Could not create file storage folder. Error: "。你在自写 EA 时若复用这套结构,改 TOTAL_LANG 和列内容就能扩到中文。 注意 MSG_LIB_SYS_ERROR_FAILED_CONV_TO_LOWERCASE 与“文件已创建并加入列表”这两条在源码里被高亮,说明是调试期高频触发项;外汇与贵金属品种切换时大小写不匹配可能概率性引发该错。

MQL5 / C++
MSG_LIB_SYS_FAILED_WRITE_UARRAY_TO_FILE,                 class=class="str">"cmt">// Could not write class="type">uchar array to file
MSG_LIB_SYS_FAILED_LOAD_UARRAY_FROM_FILE,                   class=class="str">"cmt">// Could not load class="type">uchar array from file
MSG_LIB_SYS_FAILED_CREATE_OBJ_STRUCT_FROM_UARRAY,   class=class="str">"cmt">// Could not create object structure from class="type">uchar array
MSG_LIB_SYS_FAILED_SAVE_OBJ_STRUCT_TO_UARRAY,             class=class="str">"cmt">// Failed to save object structure to class="type">uchar array, error
MSG_LIB_SYS_ERROR_INDEX,                                   class=class="str">"cmt">// Error. "index" value should be within class="num">0 - class="num">3
MSG_LIB_SYS_ERROR_FAILED_CONV_TO_LOWERCASE,                class=class="str">"cmt">// Failed to convert class="type">class="kw">string to lowercase, error

MSG_LIB_SYS_ERROR_EMPTY_STRING,                            class=class="str">"cmt">// Error. Predefined symbols class="type">class="kw">string empty, to be used
MSG_LIB_SYS_FAILED_PREPARING_SYMBOLS_ARRAY,                class=class="str">"cmt">// Failed to prepare array of used symbols. Error 
MSG_LIB_SYS_INVALID_ORDER_TYPE,                            class=class="str">"cmt">// Invalid order type:

class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string messages_library[][TOTAL_LANG]=
  {
   {"Начало списка параметров","Beginning of the event parameter list"},
   {"Конец списка параметров","End of the parameter list"},
   {"Свойство не поддерживается","Property not supported"},
   {"Свойство не поддерживается в MQL4","Property not supported in MQL4"},
   {"Свойство не поддерживается у позиции","Property not supported for position"},
   {"Свойство не поддерживается у отложенного ордера","Property not supported for pending order"},
   {"Свойство не поддерживается у маркет-ордера","Property not supported for market order"},
   {"Свойство не поддерживается у исторического маркет-ордера","Property not supported for historical market order"},
   {"Значение не задано","Value not set"},
   {"Отсутствует","Not set"},

   {"Ошибка ","Error "},
   {"Ошибка. Такого символа нет на сервере","Error. No such symbol on server"},
   {"Не удалось поместить в обзор рынка. Ошибка: ","Failed to put in market watch. Error: "},
   {"Не удалось получить текущие цены. Ошибка: ","Could not get current prices. Error: "},
   {"Не удалось получить коэффициенты взимания маржи. Ошибка: ","Failed to get margin rates. Error: "},
   {"Не удалось получить данные ","Failed to get data of "},

   {"Не удалось создать папку хранения файлов. Ошибка: ","Could not create file storage folder. Error: "},
   {"Ошибка. Не удалось добавить текущий объект-аккаунт в список-коллекцию","Error. Failed to add current account object to collection list"},
   {"Ошибка. Не удалось создать объект-аккаунт с данными текущего счёта","Error. Failed to create account object with current account data"},
   {"Не удалось открыть для записи файл ","Could not open file for writing: "},
   {"Ошибка входных данных: нет символа ","Input error: no "},
   {"Не удалось создать объект-символ ","Failed to create symbol object "},
   {"Не удалось добавить символ ","Failed to add "},

   {"Не удалось получить текущие цены по символу события ","Failed to get current prices by event symbol "},
   {"Такое событие уже есть в списке","This event already in the list"},
   {"Такой файл уже создан и добавлен в список: ","This file has already been created and added to list: "}
  };

◍ 本地化报错表与音频资源头的衔接点

在自研 EA 或指标里做多语言报错,最省事的办法是把俄文原串和英文串成对写进数组,运行时按终端语言挑一条抛给 Comment() 或 Print()。上面这段就列出了二十来条典型故障:资源文件指针建不出来、计数器 ID 重复、临时列表创建失败、市场/历史集合类型不对、订单或成交塞不进列表,以及 ticks 还没来就急着读。 其中有几条值得单独留意:"index" 取值被锁在 0–3 之间,超了直接报错;字符串转小写失败也会带错误码返回,说明底层用了 StringToLower 且依赖系统区域表。这些边界在回测和实盘都可能触发,外汇和贵金属品种波动大、 tick 来得猛,初始化顺序没排好就容易先撞上 "No ticks yet"。 音频模块的头文件里挂了 coin_01、coin_02 这类下落硬币音效数组,用于成交反馈。接这套报错表时,若你打算用音效替代弹窗,记得先在 Terminal 里测一次资源指针创建,否则会先收到 "Failed to create resource file link object" 而不是听到声音。

MQL5 / C++
{ "Ошибка. Не удалось создать объект-указатель на файл ресурса", "Error. Failed to create resource file link object" },
{ "Ошибка. Уже создан счётчик с идентификатором ", "Error. Already created counter with id " },
{ "Не удалось создать счётчик таймера ", "Failed to create timer counter " },
{ "Ошибка создания временного списка", "Error creating temporary list" },
{ "Ошибка. Список не является списком рыночной коллекции", "Error. The list is not a list of market collection" },
{ "Ошибка. Список не является списком исторической коллекции", "Error. The list is not a list of history collection" },
{ "Не удалось добавить ордер в список", "Could not add order to list" },
{ "Не удалось добавить сделку в список", "Could not add deal to list" },
{ "Не удалось добавить контрольный ордер ", "Failed to add control order " },
{ "Не удалось добавить контрольую позицию ", "Failed to add control position " },
{ "Не удалось добавить модифицированный ордер в список изменённых ордеров", "Could not add modified order to list of modified orders" },
{ "Ещё не было тиков", "No ticks yet" },
{ "Не удалось создать структуру объекта", "Could not create object structure" },
{ "Не удалось записать class="type">uchar-массив в файл", "Could not write class="type">uchar array to file" },
{ "Не удалось загрузить class="type">uchar-массив из файла", "Could not load class="type">uchar array from file" },
{ "Не удалось создать структуру объекта из class="type">uchar-массива", "Could not create object structure from class="type">uchar array" },
{ "Не удалось сохранить структуру объекта в class="type">uchar-массив, ошибка ", "Failed to save object structure to class="type">uchar array, error " },
{ "Ошибка. Значение \"index\" должно быть в пределах class="num">0 - class="num">3", "Error. \"index\" value should be between class="num">0 - class="num">3" },
{ "Не удалось преобразовать строку в нижний регистр, ошибка ", "Failed to convert class="type">class="kw">string to lowercase, error " },
{ "Ошибка. Строка предопределённых символов пустая, будет использоваться ", "Error. String of predefined symbols is empty, symbol will be used: " },
{ "Не удалось подготовить массив используемых символов. Ошибка ", "Failed to create array of used characters. Error " },
{ "Неправильный тип ордера: ", "Invalid order type: " }

常见问题

用文件生成器类把音频资源转成头文件里的字节数组,编译时一并进 EX5,运行时再写出到临时目录调用。
多查系统库报错枚举里的隐性失败分支,很多资源写入失败走的是静默返回,需手动打印枚举值定位。
可以,把本地化报错表和音频资源头丢给小布,它会比对枚举键与消息表键,标出未衔接的缺失项。
按枚举 ID 做键、语言代码做二级键写成结构化文件,运行时按当前语言加载,避免硬编码在代码里。
建议分开,图像走像素流、音频走采样流,共用一个资源头写入接口即可,降低单类复杂度。