神经网络变得轻松(第二十三部分):构建迁移学习工具·进阶篇
📘

神经网络变得轻松(第二十三部分):构建迁移学习工具·进阶篇

第 2/3 篇

神经网络面板里的控件与标签生成

在 MT5 自建神经网络搭建面板时,先得把界面控件声明清楚。下面这段类成员定义了编辑框、下拉框、按钮和列表视图:m_edBatch 与 m_edProbability 负责批大小与概率参数输入,m_cbActivation 和 m_cbOptimization 切换激活函数与优化器,m_btAddLayer / m_btDeleteLayer 控制层数增减,m_lstNewModel 展示新建模型结构,m_btSave 存盘,m_arAddLayers 用 CArrayObj 托管动态层对象。 面板的 Create 与 OnEvent 被声明为虚函数,说明子类可重写交互逻辑;Destroy 直接转调 CAppDialog::Destroy,Run 与 ChartEvent 也只是薄封装,真正事件路由仍交给基类。 标签不是手填资源,而是靠 CreateLabel 批量造:传入 id、文本和坐标,函数内部 new 一个 CLabel,用 StringFormat("%s%d", LABEL_NAME, id) 自动拼出唯一控件名。任意一步 Create / Text / Add 失败就 delete 并返回 false,避免悬空指针。 在 MT5 里跑这套,你可以把 LABEL_NAME 前缀改掉,再数一下同面板最多能叠多少个 label——控件 ID 从 0 自增,超过 2^31 之前都不会撞名,但界面重绘开销会随标签数线性上升。

MQL5 / C++
  CEdit                 m_edBatch;
  CEdit                 m_edProbability;
  CComboBox             m_cbActivation;
  CComboBox             m_cbOptimization;
  CButton               m_btAddLayer;
  CButton               m_btDeleteLayer;
  class=class="str">"cmt">//--- new model
  CListView             m_lstNewModel;
  CButton               m_btSave;
CArrayObj         m_arAddLayers;
class="kw">public:
                    CNetCreatorPanel();
                   ~CNetCreatorPanel();
  class=class="str">"cmt">//--- main application dialog creation and destroy
  class="kw">virtual class="type">bool      Create(class="kw">const class="type">long chart, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int subwin, class="kw">const class="type">int x1, class="kw">const class="type">int y1);
  class=class="str">"cmt">//--- chart event handler
  class="kw">virtual class="type">bool      OnEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam);

  class="kw">virtual class="type">void      Destroy(class="kw">const class="type">int reason = REASON_PROGRAM) class="kw">override { CAppDialog::Destroy(reason); }
  class="type">bool              Run(class="type">void) { class="kw">return CAppDialog::Run();}
  class="type">void              ChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
   {                CAppDialog::ChartEvent(id, lparam, dparam, sparam); }
  };
class="type">bool CNetCreatorPanel::CreateLabel(class="kw">const class="type">int id, class="kw">const class="type">class="kw">string text, class="kw">const class="type">int x1, class="kw">const class="type">int y1, class="kw">const class="type">int x2, class="kw">const class="type">int y2)
  {
   CLabel *tmp_label = new CLabel();
   if(!tmp_label)
      class="kw">return class="kw">false;
   if(!tmp_label.Create(m_chart_id, StringFormat("%s%d", LABEL_NAME, id), m_subwin, x1, y1, x2, y2))
     {
       class="kw">delete tmp_label;
       class="kw">return class="kw">false;
     }
   if(!tmp_label.Text(text))
     {
       class="kw">delete tmp_label;
       class="kw">return class="kw">false;
     }
   if(!Add(tmp_label))
     {
       class="kw">delete tmp_label;
       class="kw">return class="kw">false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
  }
class="type">bool CNetCreatorPanel::CreateEdit(class="kw">const class="type">int id,
                                 CEdit& object,

「面板里编辑框与神经元下拉框的落地写法」

在 MT5 自定义面板里塞一个可编辑文本框,核心不是画出来,而是把坐标、只读属性和对象注册一次性跑通。下面这段把 x1/y1/x2/y2 作为控件矩形对角坐标传入,用 StringFormat 拼出唯一名避免和同图其他 EDIT 重名,ALIGN_RIGHT 让文字贴右,ReadOnly 决定用户能否改。 [CODE] const int x1, const int y1, const int x2, const int y2, bool read_only) { if(!object.Create(m_chart_id, StringFormat("%s%d", EDIT_NAME, id), m_subwin, x1, y1, x2, y2)) return false; if(!object.TextAlign(ALIGN_RIGHT)) return false; if(!object.ReadOnly(read_only)) return false; if(!Add(object)) return false; //--- return true; } [/CODE] 逐行拆解:前四行是函数入参,x1/y1 为左上角、x2/y2 为右下角像素坐标,read_only 控制交互权限。Create 调用在指定图表子窗口 m_subwin 建对象,名用 EDIT_NAME 加 id 防重。TextAlign(ALIGN_RIGHT) 设右对齐,ReadOnly 设只读态,Add 把对象挂进面板容器,任何一步失败直接返 false。 下拉框则要预灌神经元类型。CreateComboBoxType 里给 m_cbNewNeuronType 一次性 ItemAdd 了 11 种层类型:从 Base、Conv、Proof 到 LSTM、Attention、多头注意力、MLMH 注意力,再到 Dropout、BatchNorm、VAE。 [CODE] bool CNetCreatorPanel::CreateComboBoxType(const int x1, const int y1, const int x2, const int y2) { if(!m_cbNewNeuronType.Create(m_chart_id, "cbNewNeuronType", m_subwin, x1, y1, x2, y2)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronBaseOCL), defNeuronBaseOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronConvOCL), defNeuronConvOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronProofOCL), defNeuronProofOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronLSTMOCL), defNeuronLSTMOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronAttentionOCL), defNeuronAttentionOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronMHAttentionOCL), defNeuronMHAttentionOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronMLMHAttentionOCL), defNeuronMLMHAttentionOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronDropoutOCL), defNeuronDropoutOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronBatchNormOCL), defNeuronBatchNormOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronVAEOCL), defNeuronVAEOCL)) return false; if(!Add(m_cbNewNeuronType)) return false; //--- return true; } [/CODE] ItemAdd 第一个参数是显示字符串(由 LayerTypeToString 从枚举转),第二个是实际枚举值。11 次添加若任一项失败面板构建即中断,最后 Add 进容器才返 true。开 MT5 把这段塞进你的 CNetCreatorPanel 类,编译后拖面板出来就能看到下拉里齐了 11 种层——外汇与贵金属行情高波动,这类工具仅辅助结构搭建,实盘信号仍须自担风险。

MQL5 / C++
class="kw">const class="type">int x1,
class="kw">const class="type">int y1,
class="kw">const class="type">int x2,
class="kw">const class="type">int y2,
class="type">bool read_only)
{
 if(!object.Create(m_chart_id, StringFormat("%s%d", EDIT_NAME, id), m_subwin, x1, y1, x2, y2))
   class="kw">return class="kw">false;
 if(!object.TextAlign(ALIGN_RIGHT))
   class="kw">return class="kw">false;
 if(!object.ReadOnly(read_only))
   class="kw">return class="kw">false;
 if(!Add(object))
   class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="kw">return true;
}
class="type">bool CNetCreatorPanel::CreateComboBoxType(class="kw">const class="type">int x1, class="kw">const class="type">int y1, class="kw">const class="type">int x2, class="kw">const class="type">int y2)
{
 if(!m_cbNewNeuronType.Create(m_chart_id, "cbNewNeuronType", m_subwin, x1, y1, x2, y2))
   class="kw">return class="kw">false;
 if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronBaseOCL), defNeuronBaseOCL))
   class="kw">return class="kw">false;
 if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronConvOCL), defNeuronConvOCL))
   class="kw">return class="kw">false;
 if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronProofOCL), defNeuronProofOCL))
   class="kw">return class="kw">false;
 if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronLSTMOCL), defNeuronLSTMOCL))
   class="kw">return class="kw">false;
 if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronAttentionOCL), defNeuronAttentionOCL))
   class="kw">return class="kw">false;
 if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronMHAttentionOCL), defNeuronMHAttentionOCL))
   class="kw">return class="kw">false;
 if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronMLMHAttentionOCL), defNeuronMLMHAttentionOCL))
   class="kw">return class="kw">false;
 if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronDropoutOCL), defNeuronDropoutOCL))
   class="kw">return class="kw">false;
 if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronBatchNormOCL), defNeuronBatchNormOCL))
   class="kw">return class="kw">false;
 if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronVAEOCL), defNeuronVAEOCL))
   class="kw">return class="kw">false;
 if(!Add(m_cbNewNeuronType))
   class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="kw">return true;
}

◍ 神经网络面板初始化时的控件布局逻辑

在 MT5 自定义对话框里搭一个神经网络配置面板,核心就是按坐标把标签、编辑框、下拉框和列表依次堆上去。下面这段 Create 方法先调父类 CAppDialog::Create 定出整体尺寸,再用 INDENT_LEFT、INDENT_TOP 等常量算每个控件的左上角和右下角。 PANEL_WIDTH 和 PANEL_HEIGHT 决定面板总大小,LIST_WIDTH、EDIT_WIDTH、ADDS_WIDTH 以及 CONTROLS_GAP_X/Y 控制横向分区与间距。比如预训练模型名编辑框默认文本是 "Select file",Layers Total 编辑框初值写死 "0",说明建面板时还没载入任何模型结构。 自旋钮 m_spPTModelLayers 的 MinValue、MaxValue、Value 全设 0,是因为此时不知道模型有几层可迁移,得等文件读入后再动态改上限。列表 m_lstPTModel 占左侧剩余高度(ClientAreaHeight() - INDENT_BOTTOM),并开了垂直滚动,模型层多了不会撑爆界面。 右侧区域从 lx2 + CONTROLS_GAP_X 起,放「Add layer」标签、类型下拉框和神经元数编辑框(默认常量 DEFAULT_NEURONS)。这种左模型预览、右手动加层的排布,你在自己写 EA 面板时可以直接抄坐标算法。

MQL5 / C++
class="type">bool CNetCreatorPanel::Create(class="kw">const class="type">long chart, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int subwin, class="kw">const class="type">int x1, class="kw">const class="type">int y1)
  {
   if(!CAppDialog::Create(chart, name, subwin, x1, y1, x1 + PANEL_WIDTH, y1 + PANEL_HEIGHT))
      class="kw">return class="kw">false;
   class="type">int lx1 = INDENT_LEFT;
   class="type">int ly1 = INDENT_TOP;
   class="type">int lx2 = lx1 + LIST_WIDTH;
   class="type">int ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(class="num">0, "PreTrained model", lx1, ly1, lx2, ly2))
      class="kw">return class="kw">false;
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateEdit(class="num">0, m_edPTModel, lx1, ly1, lx2, ly2, true))
      class="kw">return class="kw">false;
   if(!m_edPTModel.Text("Select file"))
      class="kw">return class="kw">false;
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(class="num">1, "Layers Total", lx1, ly1, lx1 + EDIT_WIDTH, ly2))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   if(!CreateEdit(class="num">1, m_edPTModelLayers, lx2 - EDIT_WIDTH, ly1, lx2, ly2, true))
      class="kw">return class="kw">false;
   if(!m_edPTModelLayers.Text("class="num">0"))
      class="kw">return class="kw">false;
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(class="num">2, "Transfer Layers", lx1, ly1, lx1 + EDIT_WIDTH, ly2))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   if(!m_spPTModelLayers.Create(m_chart_id, "spPTMCopyLayers", m_subwin, lx2 - class="num">100, ly1, lx2, ly2))
      class="kw">return class="kw">false;
   m_spPTModelLayers.MinValue(class="num">0);
   m_spPTModelLayers.MaxValue(class="num">0);
   m_spPTModelLayers.Value(class="num">0);
   if(!Add(m_spPTModelLayers))
      class="kw">return class="kw">false;
   lx1 = INDENT_LEFT;
   lx2 = lx1 + LIST_WIDTH;
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ClientAreaHeight() - INDENT_BOTTOM;
   if(!m_lstPTModel.Create(m_chart_id, "lstPTModel", m_subwin, lx1, ly1, lx2, ly2))
      class="kw">return class="kw">false;
   if(!m_lstPTModel.VScrolled(true))
      class="kw">return class="kw">false;
   if(!Add(m_lstPTModel))
      class="kw">return class="kw">false;
   lx1 = lx2 + CONTROLS_GAP_X;
   lx2 = lx1 + ADDS_WIDTH;
   ly1 = INDENT_TOP;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(class="num">3, "Add layer", lx1, ly1, lx2, ly2))
      class="kw">return class="kw">false;
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateComboBoxType(lx1, ly1, lx2, ly2))
      class="kw">return class="kw">false;
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(class="num">4, "Neurons", lx1, ly1, lx1 + EDIT_WIDTH, ly2))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   if(!CreateEdit(class="num">2, m_edCount, lx2 - EDIT_WIDTH, ly1, lx2, ly2, class="kw">false))
      class="kw">return class="kw">false;
   if(!m_edCount.Text((class="type">class="kw">string)DEFAULT_NEURONS))

神经网络面板里的图层控件与描述提取

在 MT5 自建神经网络配置面板时,图层的新增与删除靠两个按钮对象完成。代码里 m_btAddLayerm_btDeleteLayer 分别用 Create 在子窗口坐标 (lx1, ly1) 到 (lx1+ADDS_WIDTH/2, ly2) 与对称右半区生成,文本写死为 "ADD LAYER" 与 "DELETE",且都设了 Locking(false) 允许重复点击。 描述类继承上,CNetModify 公开派生自 CNet,只补了两个接口:LayersTotal() 返回图层数、GetLayersDiscriptions() 回传 CArrayObj* 指针。后者遍历 LayersTotal() 次,每次取 layers.At(i) 的首神经元调 GetLayerInfo() 塞进数组,任一步空指针就 break 提前退出。 GetLayerInfo 的基类版把 typecount(=Output.Total())、optimizationactivation 以及 batch(LS 优化时取 iBatch,否则恒为 1)写进新 CLayerDescriptionCNeuronProofOCL 覆写时再补 window=iWindowstep=iStep。这意味着你改了窗口或步长参数,描述对象里会立刻反映,不用动面板其它逻辑。 想验证这套结构,直接把 CNetModify::GetLayersDiscriptions 抄进自己的 EA,断点看返回的 CLayerDescription 数量是否等于 LayersTotal();外汇与贵金属模型训练波动大、过拟合风险高,数值仅供本地回测参照。

MQL5 / C++
  class="kw">return class="kw">false;
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(class="num">5, "Activation", lx1, ly1, lx1 + EDIT_WIDTH, ly2))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   if(!CreateComboBoxActivation(lx2 - EDIT_WIDTH, ly1, lx2, ly2))
      class="kw">return class="kw">false;
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + BUTTON_HEIGHT;
   if(!m_btAddLayer.Create(m_chart_id, "btAddLayer", m_subwin, lx1, ly1, lx1 + ADDS_WIDTH / class="num">2, ly2))
      class="kw">return class="kw">false;
   if(!m_btAddLayer.Text("ADD LAYER"))
      class="kw">return class="kw">false;
   m_btAddLayer.Locking(class="kw">false);
   if(!Add(m_btAddLayer))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   if(!m_btDeleteLayer.Create(m_chart_id, "btDeleteLayer", m_subwin, lx2 - ADDS_WIDTH / class="num">2, ly1, lx2, ly2))
      class="kw">return class="kw">false;
   if(!m_btDeleteLayer.Text("DELETE"))
      class="kw">return class="kw">false;
   m_btDeleteLayer.Locking(class="kw">false);
   if(!Add(m_btDeleteLayer))
      class="kw">return class="kw">false;
class CNetModify :  class="kw">public CNet
  {
class="kw">public:
                     CNetModify(class="type">void) {};
                    ~CNetModify(class="type">void) {};
  class=class="str">"cmt">//---
  class="type">uint               LayersTotal(class="type">void);
  CArrayObj*         GetLayersDiscriptions(class="type">void);
  };
CLayerDescription* CNeuronBaseOCL::GetLayerInfo(class="type">void)
  {
  CLayerDescription* result = new CLayerDescription();
  if(!result)
      class="kw">return result;
class=class="str">"cmt">//---
  result.type = Type();
  result.count = Output.Total();
  result.optimization = optimization;
  result.activation = activation;
  result.batch = (class="type">int)(optimization == LS ? iBatch : class="num">1);
  result.layers = class="num">1;
class=class="str">"cmt">//---
  class="kw">return result;
  }
CLayerDescription* CNeuronProofOCL::GetLayerInfo(class="type">void)
  {
  CLayerDescription *result = CNeuronBaseOCL::GetLayerInfo();
  if(!result)
      class="kw">return result;
  result.window = (class="type">int)iWindow;
  result.step = (class="type">int)iStep;
class=class="str">"cmt">//---
  class="kw">return result;
  }
CArrayObj* CNetModify::GetLayersDiscriptions(class="type">void)
  {
  CArrayObj* result = new CArrayObj();
  for(class="type">uint i = class="num">0; i < LayersTotal(); i++)
     {
      CLayer* layer = layers.At(i);
      if(!layer)
         class="kw">break;
      CNeuronBaseOCL* neuron = layer.At(class="num">0);
      if(!neuron)
         class="kw">break;
      if(!result.Add(neuron.GetLayerInfo()))
         class="kw">break;
     }
class=class="str">"cmt">//---
  class="kw">return result;
  }
class="type">bool CNetCreatorPanel::LoadModel(class="type">class="kw">string file_name)
  {
  class="type">class="kw">float error, undefine, forecast;
  class="type">class="kw">datetime time;
  ResetLastError();

「预训练模型载入与图层管理的代码落点」

在 MT5 里做神经网络面板时,载入 .nnw 预训练模型最怕遇到文件损坏却无任何提示。下面这段逻辑会在 Load 失败时先清空列表、写入错误文案,再用 GetLastError 区分「文件损坏(err==0)」和「其他错误码」两种情形,交易者调试自己的模型文件时可直接照抄这个分支结构。 [CODE] if(!m_Model.Load(file_name, error, undefine, forecast, time, false)) { m_lstPTModel.ItemsClear(); m_lstPTModel.ItemAdd("Error of load model", 0); m_lstPTModel.ItemAdd(file_name, 1); int err = GetLastError(); if(err == 0) m_lstPTModel.ItemAdd("The file is damaged"); else m_lstPTModel.ItemAdd(StringFormat("error id: %d", GetLastError()), 2); m_edPTModel.Text("Select file"); return false; } [/CODE] 载入成功后才去读层数:m_Model.LayersTotal() 返回整数,写入编辑框;随后用 GetLayersDiscriptions 拿描述数组,循环拼出「类型 (units N)」的条目塞进列表。外汇与贵金属行情的高波动特性,可能让训练好的模型在样本外失效,因此图层结构可视化是排查泛化问题的第一步。 OpenPreTrainedModel 用 FileSelectDialog 限定后缀 *.nnw,只取 filenames[0] 交给 LoadModel;事件映射里把 m_edPTModel 的点击绑到这个函数,点编辑框就弹文件框。CNetModify 的 GetLayer / AddLayer 则负责运行时按索引取层或追加新层,索引越界直接返 NULL,避免空指针崩面板。

MQL5 / C++
if(!m_Model.Load(file_name, error, undefine, forecast, time, class="kw">false))
  {
  m_lstPTModel.ItemsClear();
  m_lstPTModel.ItemAdd("Error of load model", class="num">0);
  m_lstPTModel.ItemAdd(file_name, class="num">1);
  class="type">int err = GetLastError();
  if(err == class="num">0)
     m_lstPTModel.ItemAdd("The file is damaged");
  else
     m_lstPTModel.ItemAdd(StringFormat("error id: %d", GetLastError()), class="num">2);
  m_edPTModel.Text("Select file");
  class="kw">return class="kw">false;
  }
 m_edPTModel.Text(file_name);
 m_edPTModelLayers.Text((class="type">class="kw">string)m_Model.LayersTotal());
 if(!!m_arPTModelDescription)
   class="kw">delete m_arPTModelDescription;
 m_arPTModelDescription = m_Model.GetLayersDiscriptions();
 m_lstPTModel.ItemsClear();
 class="type">int total = m_arPTModelDescription.Total();
 for(class="type">int i = class="num">0; i < total; i++)
   {
   CLayerDescription* temp = m_arPTModelDescription.At(i);
   if(!temp)
     class="kw">return class="kw">false;
   class=class="str">"cmt">//---
   class="type">class="kw">string item = StringFormat("%s(units %d)", LayerTypeToString(temp.type), temp.count);
   if(!m_lstPTModel.AddItem(item, i))
     class="kw">return class="kw">false;
   }
 m_spPTModelLayers.MaxValue(total);
 m_spPTModelLayers.Value(total);
class=class="str">"cmt">//---
 class="kw">return true;
 }
class="type">bool CNetCreatorPanel::OpenPreTrainedModel(class="type">void)
  {
  class="type">class="kw">string filenames[];
  if(FileSelectDialog("Select a file to load data", NULL,
                       "Neuron Net(*.nnw)|*.nnw|All files(*.*)|*.*",
                       FSD_FILE_MUST_EXIST, filenames, NULL) > class="num">0)
   {
   if(!LoadModel(filenames[class="num">0]))
     class="kw">return class="kw">false;
   }
  else
   m_edPTModel.Text("Files not selected");
class=class="str">"cmt">//---
  class="kw">return true;
  }
EVENT_MAP_BEGIN(CNetCreatorPanel)
ON_EVENT(ON_CLICK, m_edPTModel, OpenPreTrainedModel)
ON_EVENT(ON_CLICK, m_btAddLayer, OnClickAddButton)
ON_EVENT(ON_CLICK, m_btDeleteLayer, OnClickDeleteButton)
ON_EVENT(ON_CLICK, m_btSave, OnClickSaveButton)
ON_EVENT(ON_CHANGE, m_spPTModelLayers, ChangeNumberOfLayers)
ON_EVENT(ON_CHANGE, m_lstPTModel, OnChangeListPTModel)
EVENT_MAP_END(CAppDialog)
CLayer* CNetModify::GetLayer(class="type">uint layer)
  {
  if(!layers || LayersTotal() <= layer)
   class="kw">return NULL;
class=class="str">"cmt">//---
  class="kw">return layers.At(layer);
  }
class="type">bool CNetModify::AddLayer(CLayer *new_layer)
  {
  if(!new_layer)
   class="kw">return class="kw">false;
  if(!layers)
   {
   layers = new CArrayLayer();
   if(!layers)
     class="kw">return class="kw">false;
   }
class=class="str">"cmt">//---
  class="kw">return layers.Add(new_layer);
  }

常见问题

在面板初始化时按图层数组循环创建下拉控件,每个选项绑定该层可设的神经元区间,用 CComboBox 类逐个 Add 即可。
给编辑框挂 OnChange 事件,回调里读取值并写回对应图层结构体,同时重绘标签描述,避免只改界面不改模型。
可以。小布能按你给的图层配置直接产出控件布局代码与标签提取逻辑,你只核对参数落点就行。
放在面板控件创建完成、描述标签提取之前,先填充图层数组再渲染下拉框,否则控件会读空数据。
多是控件布局循环和描述提取顺序写反了,先建控件后取描述,或索引没随图层数组偏移就会导致错层。