创建 MQL5-Telegram 集成 EA 交易 (第二部分):从 MQL5 发送信号到 Telegram·进阶篇
📨

创建 MQL5-Telegram 集成 EA 交易 (第二部分):从 MQL5 发送信号到 Telegram·进阶篇

(2/3)· 承接上篇机器人搭建,本篇把均線交叉信号打包进单条 Telegram 消息并直发群聊

含代码示例实战向 第 2/3 篇

接上篇,我们继续深挖信号出口。很多人卡在 MQL5 只能发单条短消息,一长串信号拆成几段发就报错,实盘里根本来不及看。本篇把移动平均交叉的买卖与成交合成一条结构化消息推到 Telegram,几乎实时到账。

MT5里把字符串塞进URL的编码坑

在MT5里给WebRequest拼URL参数时,中文或特殊符号直接拼进去会直接废掉请求。这段UrlEncode函数就是按RFC 3986的非保留字符规则,把0-9、A-Z、a-z以及! ' ( ) * - . _ ~原样保留,空格转成'+',其余统一走UTF-8百分号编码。 代码里用StringGetCharacter逐个取ushort,数字判定区间是48–57,大写65–90,小写97–122,命中就ShortToString直贴,没命中的走ShortToUtf8拿字节数组再用StringFormat("%%%02X")转成%XX。 实测一个汉字如'布'会被拆成3个字节、输出类似%E5%B8%83,直接贴进MT5的WebRequest地址栏就能被服务端正确解析。外汇与贵金属行情接口调用属高风险操作,参数拼错可能触发异常报价或空响应,先在策略测试器里跑一遍再上实盘。

MQL5 / C++
class="type">class="kw">string UrlEncode(class="kw">const class="type">class="kw">string text) {
   class="type">class="kw">string encodedText = "";
   class="type">int textLength = StringLen(text);
   for (class="type">int i = class="num">0; i < textLength; i++) {
      class="type">class="kw">ushort character = StringGetCharacter(text, i);
      if ((character >= class="num">48 && character <= class="num">57) ||
          (character >= class="num">65 && character <= class="num">90) ||
          (character >= class="num">97 && character <= class="num">122) ||
          character == &class="macro">#x27;!&class="macro">#x27; || character == &class="macro">#x27;\&class="macro">#x27;&class="macro">#x27; || character == &class="macro">#x27;(&class="macro">#x27; ||
          character == &class="macro">#x27;)&class="macro">#x27; || character == &class="macro">#x27;*&class="macro">#x27; || character == &class="macro">#x27;-&class="macro">#x27; ||
          character == &class="macro">#x27;.&class="macro">#x27; || character == &class="macro">#x27;_&class="macro">#x27; || character == &class="macro">#x27;~&class="macro">#x27;) {
         encodedText += ShortToString(character);
      }
      else if (character == &class="macro">#x27; &class="macro">#x27;) {
         encodedText += ShortToString(&class="macro">#x27;+&class="macro">#x27;);
      }
      else {
         class="type">uchar utf8Bytes[];
         class="type">int utf8Length = ShortToUtf8(character, utf8Bytes);
         for (class="type">int j = class="num">0; j < utf8Length; j++) {
            encodedText += StringFormat("%%%02X", utf8Bytes[j]);
         }
      }
   }
   class="kw">return encodedText;
}

◍ 把字符塞进 URL 前的字节拆解

在 MT5 里往 WebRequest 传中文或特殊符号前,得先做成百分号编码。核心思路是把每个 UTF-8 字节转成 %XX 形态,下面这段就是拼接动作:遍历字节数组,用 StringFormat("%%%02X", utf8Bytes[j]) 把单字节格式化为两位大写十六进制并前缀 %。 // Convert each byte to its hexadecimal representation prefixed with '%' encodedText += StringFormat("%%%02X", utf8Bytes[j]); 真正决定字节数的是 ShortToUtf8 这个函数。它按 Unicode 码点区间切分:小于 0x80 走 1 字节,小于 0x800 走 2 字节(首字节 0xC0 起,次字节 0x80 起),小于 0xFFFF 走 3 字节。代理区 0xD800–0xDFFF 被判定为非法,直接替换成空格返回长度 1;而 0xE000–0xF8FF 这类私有区/emoji 会被抬到 0x10000 以上扩成 4 字节输出。 实盘写 EA 调外部 API 时,若漏掉代理区处理,编码串里可能混入乱码触发 400。开 MT5 新建脚本把这段逻辑跑一遍,用 Print 看 '€'(0x20AC)和 emoji 各自吐出几个字节,就能确认你的终端是不是同一套规则。

MQL5 / C++
class=class="str">"cmt">// Convert each byte to its hexadecimal representation prefixed with &class="macro">#x27;%&class="macro">#x27;
encodedText += StringFormat("%%%02X", utf8Bytes[j]);

class="type">int ShortToUtf8(class="kw">const class="type">class="kw">ushort character, class="type">uchar &utf8Output[]) {
  class=class="str">"cmt">// Handle single byte characters(0x00 to 0x7F)
  if (character < 0x80) {
    ArrayResize(utf8Output, class="num">1);
    utf8Output[class="num">0] = (class="type">uchar)character;
    class="kw">return class="num">1;
  }
  class=class="str">"cmt">// Handle two-byte characters(0x80 to 0x7FF)
  if (character < 0x800) {
    ArrayResize(utf8Output, class="num">2);
    utf8Output[class="num">0] = (class="type">uchar)((character >> class="num">6) | 0xC0);
    utf8Output[class="num">1] = (class="type">uchar)((character & 0x3F) | 0x80);
    class="kw">return class="num">2;
  }
  class=class="str">"cmt">// Handle three-byte characters(0x800 to 0xFFFF)
  if (character < 0xFFFF) {
    if (character >= 0xD800 && character <= 0xDFFF) {
      ArrayResize(utf8Output, class="num">1);
      utf8Output[class="num">0] = &class="macro">#x27; &class="macro">#x27;;
      class="kw">return class="num">1;
    }
    else if (character >= 0xE000 && character <= 0xF8FF) {
      class="type">int extendedCharacter = 0x10000 | character;
      ArrayResize(utf8Output, class="num">4);
      utf8Output[class="num">0] = (class="type">uchar)(0xF0 | (extendedCharacter >> class="num">18));
      utf8Output[class="num">1] = (class="type">uchar)(0x80 | ((extendedCharacter >> class="num">12) & 0x3F));
    }
  }
  class="kw">return class="num">0;
}

「宽字符落进 UTF-8 字节流的拆分逻辑」

把 Unicode 码点塞进 UTF-8 字节序列,本质是按码值区间决定前缀位和移位掩码。上面这段对大于三字节的辅助平面字符,用 0x80(extendedCharacter>>6)&0x3F 取第三字节、0x80(extendedCharacter&0x3F) 取第四字节,返回长度 4;落在 0x800–0xFFFF 的 BMP 字符则走三字节分支,首字节固定 0xE0 前缀。

非法码点不要静默丢弃,这里统一替换为 U+FFFD 的字节序列 0xEF 0xBF 0xBD,长度也是 3。做 EA 日志或向文件写中文 / 俄文标签时,若少了这步,MT5 终端可能显示乱码或截断。 ShortToUtf8 入口先判单字节(<0x80 直接落盘返回 1),再判双字节(<0x800 首字节 0xC0 前缀返回 2),最后才进三字节分支。开 MT5 新建脚本,把这段贴进去跑 ShortToUtf8(0x4F60, out) 应能拿到「你」的 3 字节 UTF-8(0xE4 0xBD 0xA0),可直接验证编码路径。

MQL5 / C++
utf8Output[class="num">2] = (class="type">uchar)(0x80 | ((extendedCharacter >> class="num">6) & 0x3F)); class=class="str">"cmt">// Store the third byte
utf8Output[class="num">3] = (class="type">uchar)(0x80 | (extendedCharacter & 0x3F)); class=class="str">"cmt">// Store the fourth byte
class="kw">return class="num">4; class=class="str">"cmt">// Return the length of the UTF-class="num">8 representation
}
else {
   ArrayResize(utf8Output, class="num">3); class=class="str">"cmt">// Resize the array to hold three bytes
   utf8Output[class="num">0] = (class="type">uchar)((character >> class="num">12) | 0xE0); class=class="str">"cmt">// Store the first byte
   utf8Output[class="num">1] = (class="type">uchar)(((character >> class="num">6) & 0x3F) | 0x80); class=class="str">"cmt">// Store the second byte
   utf8Output[class="num">2] = (class="type">uchar)((character & 0x3F) | 0x80); class=class="str">"cmt">// Store the third byte
   class="kw">return class="num">3; class=class="str">"cmt">// Return the length of the UTF-class="num">8 representation
}
class=class="str">"cmt">// Handle invalid characters by replacing with the Unicode replacement character(U+FFFD)
ArrayResize(utf8Output, class="num">3); class=class="str">"cmt">// Resize the array to hold three bytes
utf8Output[class="num">0] = 0xEF; class=class="str">"cmt">// Store the first byte
utf8Output[class="num">1] = 0xBF; class=class="str">"cmt">// Store the second byte
utf8Output[class="num">2] = 0xBD; class=class="str">"cmt">// Store the third byte
class="kw">return class="num">3; class=class="str">"cmt">// Return the length of the UTF-class="num">8 representation
class=class="str">"cmt">//+-----------------------------------------------------------------------+
class=class="str">"cmt">//| Function to convert a class="type">class="kw">ushort character to its UTF-class="num">8 representation     |
class=class="str">"cmt">//+-----------------------------------------------------------------------+
class="type">int ShortToUtf8(class="kw">const class="type">class="kw">ushort character, class="type">uchar &utf8Output[]) {
   class=class="str">"cmt">// Handle single byte characters(0x00 to 0x7F)
   if (character < 0x80) {
      ArrayResize(utf8Output, class="num">1); class=class="str">"cmt">// Resize the array to hold one byte
      utf8Output[class="num">0] = (class="type">uchar)character; class=class="str">"cmt">// Store the character in the array
      class="kw">return class="num">1; class=class="str">"cmt">// Return the length of the UTF-class="num">8 representation
   }
   class=class="str">"cmt">// Handle two-byte characters(0x80 to 0x7FF)
   if (character < 0x800) {
      ArrayResize(utf8Output, class="num">2); class=class="str">"cmt">// Resize the array to hold two bytes
      utf8Output[class="num">0] = (class="type">uchar)((character >> class="num">6) | 0xC0); class=class="str">"cmt">// Store the first byte
      utf8Output[class="num">1] = (class="type">uchar)((character & 0x3F) | 0x80); class=class="str">"cmt">// Store the second byte
      class="kw">return class="num">2; class=class="str">"cmt">// Return the length of the UTF-class="num">8 representation
   }
   class=class="str">"cmt">// Handle three-byte characters(0x800 to 0xFFFF)

UTF-8 落字节时的越界与私用区处理

在把单字符写进 UTF-8 输出数组时,先卡一道码点上限:小于 0xFFFF 才走双字节/三字节分支,否则直接按非法字符塞 U+FFFD(字节序列 0xEF 0xBF 0xBD,占 3 字节)。这一步能拦掉大部分代理区错位和超 BMP 的异常输入。 代理区 0xD800–0xDFFF 属于畸形码点,代码里不报错,而是把输出数组缩成 1 字节并写空格 ' ',返回长度 1。做 MT5 日志管道或 EA 参数名清洗时,这种静默替换比抛异常更不容易卡死主循环。 私用区 0xE000–0xF8FF 被当成 emoji 扩展:先 0x10000 | character 抬成四字节码点,再按 0xF0 引导字节拆 18/12/6 位移填四字节,返回 4。其余 BMP 字符走标准三字节拆法(移 12、6 位,掩 0x3F,分别或 0xE0 / 0x80),返回 3。 下面这段是分支内的核心落字节逻辑,逐行拆完可直接粘进自定义字符编码函数里验证。注意外汇/贵金属 EA 跑这套转换时行情跳动并发高,数组反复 ArrayResize 有概率拖慢 tick 处理,建议预分配缓冲。

MQL5 / C++
if (character < 0xFFFF) {
      if (character >= 0xD800 && character <= 0xDFFF) { class=class="str">"cmt">// Ill-formed characters
            ArrayResize(utf8Output, class="num">1); class=class="str">"cmt">// Resize the array to hold one byte
            utf8Output[class="num">0] = &class="macro">#x27; &class="macro">#x27;; class=class="str">"cmt">// Replace with a space character
            class="kw">return class="num">1; class=class="str">"cmt">// Return the length of the UTF-class="num">8 representation
      }
      else if (character >= 0xE000 && character <= 0xF8FF) { class=class="str">"cmt">// Emoji characters
            class="type">int extendedCharacter = 0x10000 | character; class=class="str">"cmt">// Extend the character to four bytes
            ArrayResize(utf8Output, class="num">4); class=class="str">"cmt">// Resize the array to hold four bytes
            utf8Output[class="num">0] = (class="type">uchar)(0xF0 | (extendedCharacter >> class="num">18)); class=class="str">"cmt">// Store the first byte
            utf8Output[class="num">1] = (class="type">uchar)(0x80 | ((extendedCharacter >> class="num">12) & 0x3F)); class=class="str">"cmt">// Store the second byte
            utf8Output[class="num">2] = (class="type">uchar)(0x80 | ((extendedCharacter >> class="num">6) & 0x3F)); class=class="str">"cmt">// Store the third byte
            utf8Output[class="num">3] = (class="type">uchar)(0x80 | (extendedCharacter & 0x3F)); class=class="str">"cmt">// Store the fourth byte
            class="kw">return class="num">4; class=class="str">"cmt">// Return the length of the UTF-class="num">8 representation
      }
      else {
            ArrayResize(utf8Output, class="num">3); class=class="str">"cmt">// Resize the array to hold three bytes
            utf8Output[class="num">0] = (class="type">uchar)((character >> class="num">12) | 0xE0); class=class="str">"cmt">// Store the first byte
            utf8Output[class="num">1] = (class="type">uchar)(((character >> class="num">6) & 0x3F) | 0x80); class=class="str">"cmt">// Store the second byte
            utf8Output[class="num">2] = (class="type">uchar)((character & 0x3F) | 0x80); class=class="str">"cmt">// Store the third byte
            class="kw">return class="num">3; class=class="str">"cmt">// Return the length of the UTF-class="num">8 representation
      }
}
class=class="str">"cmt">// Handle invalid characters by replacing with the Unicode replacement character(U+FFFD)
ArrayResize(utf8Output, class="num">3); class=class="str">"cmt">// Resize the array to hold three bytes
utf8Output[class="num">0] = 0xEF; class=class="str">"cmt">// Store the first byte
utf8Output[class="num">1] = 0xBF; class=class="str">"cmt">// Store the second byte
utf8Output[class="num">2] = 0xBD; class=class="str">"cmt">// Store the third byte
class="kw">return class="num">3; class=class="str">"cmt">// Return the length of the UTF-class="num">8 representation

◍ 用上一根K线收阳收阴驱动首单

这段逻辑把账户权益、空闲保证金和图表品种先抓出来,拼成一段带 emoji 的初始化播报,再对消息做 UrlEncode 方便推送到外部通道。外汇与贵金属杠杆高,空闲保证金随时因浮亏缩水,实盘前应在 MT5 里打印 AccountInfoDouble(ACCOUNT_MARGIN_FREE) 确认数值符合预期。 信号判定只看前一根已完成 K 线:开盘价低于收盘价记为买信号,高于或等于收盘价记为卖信号。也就是说,上一根收阳就做多,收阴或十字就做空,属于极简的裸 K 反转假设,胜率随品种和周期波动,没有保本属性。 下单部分写死 0.01 手,以 Ask/Bid 市价进场,止损止盈统一拉开 1000 点(_Point)。在 XAUUSD 这类点值大的品种上 1000 点可能是 10 美元空间,而在 EURUSD 可能仅 10 点,参数必须按品种重算。 [CODE] double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); string msg = "🚀EA INITIALIZED ON CHART " + _Symbol + " 🚀" +"\n📊Account Status 📊" +"\nEquity: $" +DoubleToString(accountEquity,2) +"\nFree Margin: $" +DoubleToString(accountFreeMargin,2); string encloded_msg = UrlEncode(msg); msg = encloded_msg; double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY); double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); string msg = "\xF680 EA INITIALIZED ON CHART " + _Symbol + "\xF680" +"\n\xF4CA Account Status \xF4CA" +"\nEquity: $" +DoubleToString(accountEquity,2) +"\nFree Margin: $" +DoubleToString(accountFreeMargin,2); string encloded_msg = UrlEncode(msg); msg = encloded_msg; double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); double Price_Open = iOpen(_Symbol,_Period,1); double Price_Close = iClose(_Symbol,_Period,1); bool isBuySignal = Price_Open < Price_Close; bool isSellSignal = Price_Open >= Price_Close; #include <Trade/Trade.mqh> CTrade obj_Trade; double lotSize = 0, openPrice = 0,stopLoss = 0,takeProfit = 0; if (isBuySignal == true){ lotSize = 0.01; openPrice = Ask; stopLoss = Bid-1000*_Point; takeProfit = Bid+1000*_Point; obj_Trade.Buy(lotSize,_Symbol,openPrice,stopLoss,takeProfit); } else if (isSellSignal == true){ lotSize = 0.01; openPrice = Bid; stopLoss = Ask+1000*_Point; takeProfit = Ask-1000*_Point; obj_Trade.Sell(lotSize,_Symbol,openPrice,stopLoss,takeProfit); } string position_type = isBuySignal ? "Buy" : "Sell"; ushort MONEYBAG = 0xF4B0; string MONEYBAG_Emoji_code = ShortToString(MONEYBAG); string msg = "\xF680 OPENED "+position_type+" POSITION." +"\n====================" [/CODE] 逐行拆解:AccountInfoDouble 取账户双精度字段,ACCOUNT_MARGIN_FREE 是未占用保证金;_Symbol 是当前图表品种,DoubleToString 第二参 2 表示保留两位小数。UrlEncode 把含 emoji 的字符串转成可传输编码。iOpen/iClose 的第三参 1 代表倒数第二根(已收盘)K 线。CTrade 是标准交易类,Buy/Sell 入参依次为手数、品种、开价、止损、止盈。_Point 是最小报价单位,乘 1000 即 1000 点偏移。 别把 1000 点当通用止损 不同品种 _Point 差出百倍,XAUUSD 的 _Point 通常是 0.01,1000 点=10 美元;EURUSD 的 _Point 是 0.00001,1000 点才 0.1 美元。直接抄这段代码在黄金上可能止损过大,复制前先 Print(_Point) 看真实步长。

MQL5 / C++
class="type">class="kw">double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
class="type">class="kw">string msg = "🚀EA INITIALIZED ON CHART " + _Symbol + " 🚀"
              +"\n📊Account Status 📊"
              +"\nEquity: $"
              +DoubleToString(accountEquity,class="num">2)
              +"\nFree Margin: $"
              +DoubleToString(accountFreeMargin,class="num">2);

class="type">class="kw">string encloded_msg = UrlEncode(msg);
msg = encloded_msg;
class="type">class="kw">double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY);
class="type">class="kw">double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
class="type">class="kw">string msg = "\xF680 EA INITIALIZED ON CHART " + _Symbol + "\xF680"
              +"\n\xF4CA Account Status \xF4CA"
              +"\nEquity: $"
              +DoubleToString(accountEquity,class="num">2)
              +"\nFree Margin: $"
              +DoubleToString(accountFreeMargin,class="num">2);

class="type">class="kw">string encloded_msg = UrlEncode(msg);
msg = encloded_msg;
class="type">class="kw">double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
class="type">class="kw">double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);

class="type">class="kw">double Price_Open = iOpen(_Symbol,_Period,class="num">1);
class="type">class="kw">double Price_Close = iClose(_Symbol,_Period,class="num">1);

class="type">bool isBuySignal = Price_Open < Price_Close;
class="type">bool isSellSignal = Price_Open >= Price_Close;

class="macro">#include <Trade/Trade.mqh>
CTrade obj_Trade;
class="type">class="kw">double lotSize = class="num">0, openPrice = class="num">0,stopLoss = class="num">0,takeProfit = class="num">0;

if (isBuySignal == true){
   lotSize = class="num">0.01;
   openPrice = Ask;
   stopLoss = Bid-class="num">1000*_Point;
   takeProfit = Bid+class="num">1000*_Point;
   obj_Trade.Buy(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
}
else if (isSellSignal == true){
   lotSize = class="num">0.01;
   openPrice = Bid;
   stopLoss = Ask+class="num">1000*_Point;
   takeProfit = Ask-class="num">1000*_Point;
   obj_Trade.Sell(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
}
class="type">class="kw">string position_type = isBuySignal ? "Buy" : "Sell";

class="type">class="kw">ushort MONEYBAG = 0xF4B0;
class="type">class="kw">string MONEYBAG_Emoji_code = ShortToString(MONEYBAG);
class="type">class="kw">string msg = "\xF680 OPENED "+position_type+" POSITION."
            +"\n===================="

「正文」

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+<span class="string">"\n"</span>+MONEYBAG_Emoji_code+<span class="string">"Price = "</span>+<span class="functions">DoubleToString</span>(openPrice,<span class="predefines">_Digits</span>) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+<span class="string">"\n\xF412\Time = "</span>+<span class="functions">TimeToString</span>(<span class="functions">iTime</span>(<span class="predefines">_Symbol</span>,<span class="predefines">_Period</span>,<span class="number">0</span>),<span class="macro">TIME_SECONDS</span>) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+<span class="string">"\n\xF551\Time Current = "</span>+<span class="functions">TimeToString</span>(<span class="functions">TimeCurrent</span>(),<span class="macro">TIME_SECONDS</span>) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+<span class="string">"\n\xF525 Lotsize = "</span>+<span class="functions">DoubleToString</span>(lotSize,<span class="number">2</span>) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+<span class="string">"\n\x274E\Stop loss = "</span>+<span class="functions">DoubleToString</span>(stopLoss,<span class="predefines">_Digits</span>) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+<span class="string">"\n\x2705\Take Profit = "</span>+<span class="functions">DoubleToString</span>(takeProfit,<span class="predefines">_Digits</span>) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nb

把信号播报交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到均線交叉与推送状态,你只需在 Telegram 里收通知、做决策。

常见问题

短期均線反应快、长期均線滤噪,交叉点倾向给出趋势转向的概率信号,不叠加其他指标时逻辑最透明、也最容易写进 EA。
小布盯盘内置品种页已聚合信号与推送视图,可免去自己维护 EA 推送通道,重点放在策略而非消息格式调试。
上篇单条长消息遇特殊字符易失败且只能串行发送;合并片段后单次请求即可含信号与订单详情,降低延迟和报错概率。
在机器人令牌与网络正常时,从订单成交到群消息到达通常倾向在秒级,具体受券商与 Telegram API 限流影响。
外汇贵金属杠杆高、滑点突发,信号仅作通知不代表执行无偏差,任何播报都应配合人工核对与风控。