下载MetaTrader 5

如何将MetaTrader 5中的交易复制到MetaTrader 4

30 八月 2013, 10:27
Nikolay Demko
0
1 463

引言

曾几何时,许多交易者都认为MetaTrader 5对于实盘交易来说是一个不成熟的、不可靠的平台。但是没多久,公众看法就发生了改变,现在大家都在问MetaTrader 5何时能具备实盘交易的能力。许多交易者已经对MetaTrader 5的优点大加赞赏了。另外,由MetaQuotes软件公司举办的交易锦标赛引起了广大MQL5语言开发者的兴趣。现在,这种兴趣转化为从交易中获利的愿望。“何时MetaTrader 5才能具备实盘交易能力?”这个问题公众问错了对象。它应该由经纪商来回答。何时转到新平台上来的最终决定权在他们手上。

然而面对这种情况,一个普通交易者能做什么呢?答案显而易见,你可以利用MetaTrader 4提供的实盘交易机会,作为MetaTrader 5交易的载体。例如,写一个拷贝工具。将两个MetaTrader 4终端在Web上进行捆绑并不是一件新鲜事。现在我们要实现MetaTrader 4和MetaTrader 5之间的捆绑。

序言

为了将前面提到的想法付诸实施,有必要阐明如下问题:“利润来自哪里?”以及“交易者如何把控利润的增长?”咋一看,答案显而易见。低买高卖。但是,让我们考虑一下利润的组成部分。利润来源于买卖的价差和下单数量的乘积。也就是说,利润有两个组成部分:报价和交易头寸的交易量。

交易者能够控制什么呢?这两个组成部分哪一个才是能够被交易者把控的呢?当然是交易头寸的交易量了。报价来自于经纪商,交易者无法改变。那么得到的第一个结论是:为了实现交易复制,必须保持交易头寸交易量的同步。


1. 两个平台的比较

1.1订单计算系统的区别

由于两个平台之间的订单计算系统不一样,这样就使得复制操作复杂化了。不过我们不要忘记,这个捆绑操作是由MetaTrader 5主导的。这就意味着,在MetaTrader 4中我们实际上要重复相同的订单计算系统。

MetaTrader 5中的交易头寸可由个别交易订单得来,这一点和MetaTrader 4中的订单计算方式并不冲突。MetaTrader 5中为一个头寸设置的止赢止损,可以通过向每一个新建订单设置相同的止赢止损来实现。但当遇到问题:MetaTrader 4中什么订单需要平仓时,两个平台之间的显著差异就体现出来了。由于在MetaTrader 5中,一个交易头寸并没有独立的订单计算系统,这个问题可能成为绊脚石。

1.2交易头寸的交易量

让我仔细考虑下,平仓不同的订单会有什么不一样的结果?它将会影响我们的利润吗?例如,我们在不同时间开启了两个订单,并在不同的时间平仓了,但是某段时间内他们是同时存在的。也就是说,我们试图在订单计算系统中模拟一个交易头寸。

让我们来计算一下,如果我们改变订单的平仓位置,将对利润产生什么影响?

类型 
交易量 
开仓位置
 平仓位置
0.11.393881.38438
0.11.38868
1.38149

让我们编写一段计算代码:

void OnStart()
  {
   double open1=1.39388,close1=1.38438,
         open2=1.38868,close2=1.38149;
   Print("total  ",n1(profit((open1-close1),0.1)+profit((open2-close2),0.1)));
   Print("order2  pp=",ns(open2-close2),"  profit=",n1(profit((open2-close2),0.1)));
   Print("order1  pp=",ns(open1-close1),"  profit=",n1(profit((open1-close1),0.1)));
  }
string ns(double v){return(DoubleToString(v,_Digits));}
string n1(double v){return(DoubleToString(v,1));}
double profit(double v,double lot){return(v/_Point*lot);}
下面是计算结果:
订单1  点数=0.00950  利润=95.0
订单2  点数=0.00719  利润=71.9
合计 166.9

现在我们交换close1close2的值。

订单1  点数=0.01239  利润=123.9
订单2  点数=0.00430  利润=43.0
合计  166.9

图 1. 订单平仓的不同情形

图1显示了两种情况下AB和CD段的交易量都是0.1,而BC段的交易量都是0.2,这并不取决于哪些订单交易量被平仓的事实。

对于单一订单来说利润是不同的,但是对于整个头寸来说,总的利润是一样的。希望你注意的是,上面的例子是用两个交易量一样的订单来计算的。也就是说,在同一个位置,我们不是平仓一个订单,而是关闭了相同交易量。如果我们严格遵循平仓量的原则,那么一个订单的交易量就无关紧要了。如果要平仓的量比订单仓量大,那么会有部分平仓的情况发生。

综上所述:对于总体的持仓头寸来说,哪一个订单被平仓不重要,重要的是在给定位置的平仓量必须一致。

1.3复制交易

从上面的例子你可以看出,为了获取一样的利润,没必要传输MQL5语言写成的EA的交易信号。你只需要复制交易头寸交易量。虽然如此,但这还不是一种完全理想的交易方式,原因我们在后面会提到。然而,这些理由并不能成为,用MQL5语言编写的EA为我们赚取真实利润的阻碍。

报价的差异是利润下滑的首要因素。在某些经纪商中甚至超过了点差。原因是报价是实时复制的。当一个EA在MetaTrader 5中决定在某价位进行开仓时,MetaTrader 4终端连接的经纪商这时很可能有着不同的报价。价格对交易者来说可能有利也可能不利。

时间因素是影响利润的第二个因素。MetaTrader 5中一个头寸出现后,交易被复制,这之间的延时不可避免。

这两个因素使得头皮型策略黯然神伤。因此,在MetaTrader 5实盘交易成为可能之前,这类策略是不适用的。 

如果一个系统能够获利(从一笔交易中),且利润远大于点差并对经纪商的报价不敏感,那么使用交易复制器来赚钱将是可行的。


2. 设定问题

  1. 在MetaTrader 5和MetaTrader 4之间传输信号
  2. 从MetaTrader 5中翻译头寸信息
  3. 在MetaTrader 4中接收信号
  4. 在MetaTrader 4中复制交易头寸

2.1. 在MetaTrader 5和MetaTrader 4之间传输信号

第一步是通过共享文件传输信号。可能会问:频繁的读写不会损伤硬盘吗?如果你仅仅在头寸发生变化时写入新数据,那么这样的操作不会很频繁。不会比Windowa开发者更改系统页面文件更频繁了。这也相应的证明了不会对硬盘有损伤。如果不是非常频繁的请求共享文件以写入数据,那么这种实现方式是可以接受的。这其实也是对头皮型策略的另一个限制,虽然不像前面提到的限制条件那样明显。

你可以使用MetaTrader 5的特性,对任何深度的子目录中的文件进行读写。我不敢保证“任何”深度都行,但对10级深度子目录下的文件进行读写,肯定是没问题的。我们也不需要更深层级的了。当然你也可以使用DLL来实现。除非别无选择,否则我是不会用DLL的,这是我的基本原则。想要不通过DLL来解决这个问题,只需将MetaTrader 4安装至MetaTrader 5终端的 \Files\ 文件夹下(查看文件操作)。

那么共享文件的目录如下:

C:\Program Files\MetaTrader 5\MQL5\Files\MetaTrader 4\experts\files\xxx      // xxx是共享文件的名称。

这样,MetaTrader 4和 MetaTrader 5就都可以访问这个文件了,MQL5函数提供了文件共享的功能。

2.2. 从MetaTrader 5中翻译头寸信息

为了节约资源开销来翻译头寸,我们需要设计一个函数,来监控所有交易品种交易头寸的建立/修改/关闭。如前所述,转换一笔交易时我们只需要知道头寸的交易量。增加到交易品种的交易量以及止损止赢水平。

为了追踪变化,我们需要知道一个头寸先前的状态。如果当前状态和前一个状态不一致(即头寸发生了变化),需要在文件中记录下来。我们需要设计一个函数来将此信息写入文件。为了让多个程序同时访问,文件必须是打开状态的。

不要遗漏头寸被修改的情况,追踪系统一定要在OnTimer()函数中实现,因为我们需要同时追踪所有的交易品种,而不同交易品种的报价可能在不同的时间到来。我们还要将此种变化在文件中用符号标记一下。

2.3. 在MetaTrader 4中接收信号

有必要对文件的更新进行追踪。这可以通过设置一个变量来实现,它的状态用来监控头寸的变化。我们需要一个函数来从文件中读取头寸状态。这是一个标准函数。

将文件内容传入数组中用于计算。这里我们需要一个解析器。当从MetaTrader 5中传输内容时,把所有内容以字符串形式存储比较方便,因为不单是数字,字符也会被传输。另外,将一个交易品种的所有数据写入一个文本字符串,有助于我们避免产生混淆。

2.4. 在MetaTrader 4中复制交易头寸

这部分的函数是最复杂的。他将被分为多个子函数。

  1. 虚拟头寸比较;
  2. 订单选择函数;
  3. 订单开仓函数;
  4. 订单平仓函数;
  5. 订单修改函数。

2.4.1. 虚拟头寸比较

为了确保复制的头寸符合要求,要对虚拟头寸进行比较。函数需要单独计算每个交易品种的头寸,并且要能够过滤掉被禁止的交易(如果有的话)。

实际上,可能存在这种情况,即从MetaTrader 5传过来的交易品种,某经纪商报价系统中并不存在。这种情况下一般不应该禁止交易,但应该提供告警。用户有权知道这种情况的发生。

2.4.2. 订单选择函数

此函数用来选择基于某交易品种的订单,以便后续对他们进行进一步的操作。此种情况下,因为我们仅仅广播未平仓头寸,所以挂单必须要被过滤掉。

2.4.3. 开仓函数

它应该包含所有计算用到的参数。因此,传递头寸的交易量和类型给它就够了。

2.4.4. 订单平仓函数

就像开仓函数一样,在平仓前它需要计算所有值。

2.4.5. 订单修改函数

本函数需要包含实时市场情况的检查功能。因为许多经纪商都不允许在开仓的时候设置止损止赢,所以在开仓后再设置才是可行的。此外,同时开仓和设置止损止赢增加了下单失败的可能性。

因此,头寸很快会被重复。我们知道,设置止损止赢是必须要做的事情,它非常重要。

3. 实现

代码几乎逐行详细注释了。因此,我只解释代码中最难懂的部分。

在MetaTrader 5和MetaTrader 4之间传输信号

MetaTrader 5中的如下函数实现关联功能:

void WriteFile(string folder="Translator positions") // 默认情况下的共享文件名

打开方式参数的意义:

FILE_WRITE|FILE_SHARE_READ|FILE_ANSI

文件打开待写 | 允许不同的程序共享读取文件| ansi格式

MetaTrader 4中实现关联的函数:

int READS(string files,string &s[],bool resize)

resize参数禁止对接收到的数组数据,进行内存的重新分配。在代码中,该数组的内存根据循环计算的结果动态分配,因为开发者无法预估数组的行数。它取决于MetaTrader 5中所选择的交易品种数目。因此无法在MetaTrader 4中事先计算好。

所以,该数组在每一循环后增加一行。但是,此操作须在第二次函数调用时锁定,因为此时数组的大小已经确定并不会改变了。bool resize正是用来实现这一目的的。

从MetaTrader 5中翻译头寸信息

使用OnTimer 函数来管理头寸翻译,所有的头寸数据会以每秒一次的频率被如下函数接收

void get_positions()

对比头寸前值和现值的函数:

bool compare_positions()

当至少有一个地方不一致时,返回(true)返回(true)意味着头寸发生变化,共享文件要重写。当重写文件时,计数器cnt_command加一。

在MetaTrader 4中接收信号

当使用READS()函数读文件后,我们得到一个字符串数组s[]

为了将这些信息利用起来,我们需要一个转换器。

函数如下:

int parser(int Size)

它仅仅是一个用于调用线性识别函数的壳:

void parser_string(int x)

此函数对除交易品种名称外的所有单元进行识别。

交易品种名称在算法开始处的循环中进行一次识别,函数如下:

void parser_string_Symbols(int x)

接下来,除非特别指出,我们的讨论都是基于MQL4的。

虚拟头寸比较

头寸比较分为两个部分。在如下函数中比较头寸交易量和类型:

bool compare_positions()

在此函数中,获取头寸实际状态的函数如下:

void real_pos_volum()

根据前面提到的原则,比较函数的机制是“全有或全无”。也就是说,只要有一个地方不一致,那么所有的头寸都被认为是有变化的。在real_pos_volum()函数中使用了几个过滤器,已经在代码注释中详细描述了,并且将被其他函数反复使用。

特别的,它用于将一个交易品种的所有订单交易量求和进入一个虚拟头寸。为了正确操作锁定的头寸(如果有的话),买单交易量用减号,卖单交易量用加号。

第二部分是比较止损水平(止损水平就是止赢和止损),在类似上面的函数中实现:

bool compare_sl_tp_levels()
和交易量的情况一样,在壳内可以获取函数中止赢止损的水平信息:
void real_pos_sl_tp_levels()

订单选择函数

选择订单的唯一目的就是平仓,这也就是为什么这个复杂的函数仅仅实现平仓功能:

void close_market_order(string symbol,double lot)

整个交易品种和交易量的参数,应该平仓哪一个?为了尽可能少的拆分订单,在函数的第一个循环中,查找订单,其交易量等同于参数传递寻求保证的平仓订单,它等同于参数传递的平仓交易量的交易量。

如果没有这样的订单(平仓信号FlagLot等于状态true),那么在循环中订单指定交易量最先被平仓 (对于订单交易量是否超出的检测,则是通过Closes()函数来实现)。

选择订单修改其止赢止损的功能由如下函数实现:

void modification_sl_tp_levels()

订单仅根据交易品种名称过滤,因为同一个交易品种订单的止赢止损都是一样的。

开仓函数

实现函数如下:

int open_market_order(string symbol,int cmd,double volume,
                     int stoploss=0,int takeprofit=0,int magic=0)

它包含了对订单开仓所需的所有特定数据的检查。

订单平仓函数

实现函数如下:

bool Closes(string symbol,int ticket,double lot)

代码包含一个检查以防止lot参数超出先前选中订单的真实交易量。

订单修改函数

实现函数如下:

bool OrderTradeModif(int ticket,string symbol,int cmd,double price,
                    double stoploss=0,double takeprofit=0,int magic=0)

代码进行检查,如果止损水平和订单类型不符,则值进行对调。代码也检查水平是否已经到达请求的值。

4. 逻辑函数

至此,前面的问题解决了,但是代码中仍有一些还未解释的函数。它们是逻辑函数,我们称之为基础函数,是程序运作的核心。

void processing_signals()
void processing_sl_tp_levels()

这两个函数的特征是做无限循环,直到有条件的break退出。我们必须注意的是,脚本本身也是作为无限循环实现的。为了让用户能够方便的移除此程序,循环的主要条件就是拥有内置IsStopped()函数。

代码是以如下的方式从EA交易转移至循环脚本的:

 // Init()
 while(!IsStopped())
    {
     // Start()
     Sleep(1000);
    }
 // Deinit()

整个脚本的逻辑判断在标准函数start()中以同样的无限循环形式描述。

start() 函数中的循环代码类似如下:

     如果交易数据流空闲
          读文件,并将数据保存在数组中(不改变数组的大小);
          如果文件有变化
               写入新的注释;
               记住循环的开始时间;
               如果头寸对比不相等
                    处理此头寸的大小;
               如果头寸的止赢止损不相等;
                    处理此头寸的止赢止损;
               计算检查的结束时间;
          如果时间没有超时
               剩下的时间暂停;

最复杂的逻辑构造是位于函数processing_signals()processing_sl_tp_levels()

我们采用“从简单到复杂”的原则介绍这些个函数,虽然代码中的调用正好相反。

//+------------------------------------------------------------------+
//| 处理止赢和止损                                                     |
//+------------------------------------------------------------------+
void processing_sl_tp_levels()
  {
//--- 记住进入循环体的时间
   int start=GetTickCount();
   while(!IsStopped())
     {
      //--- 如果交易流空闲
      if(Busy_and_Connected())
        {
         //--- 选则订单并修改止赢止损           
         modification_sl_tp_levels();
        }
      //--- 如果延迟时间到,在文件中更新数据
      if(GetTickCount()-start>delay_time)READS("Translator positions",s,false);
      //--- 如果文件中标识是否有过更新的计数器发生变化,那么退出循环
      if(cnt_command!=StrToInteger(s[0]))break;
      //--- 短时休眠      
      Sleep(50);
      //---如果订单的实际止赢止损和文件中的一致,则退出循环
      if(!compare_sl_tp_levels())break;
     }
   return;
  }

正如之前提到的,函数仅在两种情况下退出无限循环:

第一种情况是cnt_command 的值和文件中保存的值不相等时。在退出之前,如果循环操作的时间超过了由全局变量delay_time设置的延迟时间时,我们需从文件中读取最新的信息。

因为所有的修改都被Busy_and_Connected()函数保护,所以可能会发生超时。也就是说,只在交易数据流空闲的时候才操作。

需要特别说明的是,在MetaTrader 4 (和MetaTrader 5相比较而言) 中,发送一系列指令到服务器,而没有一次重新报价是不可能的。服务器只会接受第一个请求,其余的将会被丢弃。因此,在向服务器发送指令前,我们必须检查一下交易数据流是否空闲。

第二种退出循环的情况是由止赢止损位比较函数compare_sl_tp_levels()控制的:如果头寸的止赢止损相等,那么退出循环。

现在我们来看最复杂的部分:processing_signals ()函数,它的构成类似,但是逻辑判断部分实现的功能迥异。

让我们来详细分析如下代码:

//--- 把文件中存储的头寸持仓方向,转换成-1,+1的形式
int TF=SymPosType[i]*2-1;
//--- 把实际头寸持仓方向转换成-1,+1的形式
int TR=realSymPosType[i]*2-1;
//--- 保存文件中的头寸大小
double VF=SymPosVol[i];
//--- 保存实际的头寸大小
double VR=realSymPosVol[i];
double lot;
//--- 如果当前货币对的头寸不相等
if(NormalizeDouble(VF*TF,8)!=NormalizeDouble(VR*TR,8))
  {
//--- 如果实际头寸大小不为零并且持仓方向不一致,或者
//--- 如果持仓方向一致,但是实际头寸大小比文件中的大
   if((VR!=0 && TF!=TR) || (TF==TR && VF<VR))
     {
      //--- 如果持仓方向一致,但是实际头寸大小比文件中的大
      if(TF==TR && VF<VR)lot=realSymPosVol[i]-SymPosVol[i];
      //--- 如果实际头寸大小不为零并且持仓方向不一致
      else lot=realSymPosVol[i];
      //--- 平仓,并退出循环
      close_market_order(Symbols[i],lot);
      break;
     }
   else
     {
      //--- 如果持仓方向一致,但是实际头寸大小比文件中的小 
      if(TF==TR && VF>VR)lot=SymPosVol[i]-realSymPosVol[i];
      //--- 如果持仓方向不一致且仓位等于零
      else lot=SymPosVol[i];
      //---开仓,并退出循环 
      open_market_order(Symbols[i],SymPosType[i],lot);
      break;
     }
  }

TFTR变量以buy=-1,sell=1的形式存储头寸类型。TF是保存在文件中的值,TR是虚拟头寸的实际的值。VFVR也一样。

因此,不相等:
if(VF*TF!=VR*TR)

将返回true如果交易量或类型不一致。

然后是逻辑连接:

if((VR!=0 && TF!=TR) || (TF==TR && VF<VR))

它表示,如果实际交易量不等于零,类型也不相同,那么你必须平仓整个头寸了。

这包括当文件中的交易量为零以及头寸方向转变的情况。当头寸方向转变时,你必须首先为开仓做准备,例如,平掉之前的持仓。在下一个迭代中,进入逻辑判断的另一个分支,开仓。

第二部分逻辑连接的复杂条件是,如果类型正确,但是实际交易量比文件中存储的大,那么你必须减少实际交易量。因此,我们首先计算减少交易量的必要手数大小。

如果没有正好满足条件的订单,并且头寸不相等,那么要新开仓。这里也有两个变量:新开整个头寸大小的订单或者在现有订单上加仓。要提醒一下,开仓函数中对是否超出开仓大小限制进行了检查,因此超出部分的仓位将会在下一个迭代中进行开仓。因为首先考虑平仓,然后才开仓,基于以上事实,锁单是几乎不可能的。

代码中有一个细节我想指出。当MetaTrader 4中的头寸刚刚根据止赢止损位平仓时,又重新开仓的情况。我之前提到过,5位数报价的差异通常是在2-3点之间。对于15点的点差来说是微不足道的。正是由于这个差异,如果止赢或者止损在MetaTrader 4中早于MetaTrader 5中触发,程序将会视图重新开仓,在MetaTrader 5的止赢或者止损触发后再次平仓。

虽然不会造成大的损失,但是会损失点差。因此,对算法进行重新设计,当头寸平仓后,MetaTrader 4不会再次恢复头寸,一直等待直到文件状态改变。只有等到那时程序才会重新开始行动。在这种情况下,如果交易者发现不对的话,可以手动平仓。在MetaTrader 5改变文件前,头寸不会被再次建立。

这样做的唯一缺点是,有一种罕见的情况,即头寸在MetaTrader 4中的止赢止损位平仓了,但是MetaTrader 5中的头寸却没有平仓。这种情况下,我建议重启Copyist positions脚本。最后要说明的是:本程序不对周末时间进行检查。这没什么影响,只是将会有产生很多无价值的重新报价日志。

5. 检查程序运行的实际情况

将MetaTrader 4安装到C:\Program Files\MetaTrader 5\MQL5\Files\ 文件夹下

在MetaTrader 5终端的任意图表(本EA的运行效果不取决于它所在的图表)上运行编译过的EATranslator positions

 

图 2. 转换MetaTrader 5上的头寸

我们看到多行注释,第一行是计数器状态,下面逐行是所有头寸的日志。

在MetaTrader 4终端的任意图表(运行效果不取决于它所在的图表)上运行Copyist positions脚本。

图 3. 在MetaTrader 4中复制头寸

现在我们可以在MetaTrader 5中运行任何EA交易。EA的运行结果很快就会复制到MetaTrader 4中。

图 4. MetaTrader 4 (顶部) 和 MetaTrader 5 (底部)中的头寸和订单

顺便说一句,MetaTrader 5的帐户管理可以手动进行,或者通过观察密码登陆。

因此,比方说,你可以在任何一个锦标赛帐户上启动复制器。


总结

本文的目的是促进交易者向新平台过渡,并鼓励其对MQL5语言进行研究。

总之我想说的是,本程序无法完全替代直接使用MetaTrader 5实盘帐户进行交易。如果不考虑逻辑判断部分,本程序是对任何交易系统适用的通用代码,因此正如一切通用的东西一样,总是会有不尽如人意的地方。但是有此作为基础,你可以为某特定的策略写一个信号转换工具。对于很多不会编程的交易者来说,可以将它作为正式版本发布前的过渡。

对于那些精通编程的人来说,我建议修改代码来使得它能够通过magic数字识别订单,并实现挂单的转换和下单。只要服务器网络连接稳定,使用挂单不会影响利润。如果连接经常断开,所有的订单包括挂单,都必须被再次复制。

让我们学习新的语言,并使用它开发出稳健的系统。祝你在交易中好运。

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/189

附加的文件 |
MetaTrader 5终端策略测试器中的订单生成算法 MetaTrader 5终端策略测试器中的订单生成算法

MetaTrader 5 通过EA交易程序和MQL5编程语言使我们可以在内嵌的策略测试器中模拟自动交易,这种模拟称为EA交易程序的测试,它可以在实现中用多线程优化,也可以在多个设备中同步进行。为了提供完整的测试,我们需要基于可用的分钟历史来生成订单。本文提供了这种算法的详细描述,即在MetaTrader 5客户终端中怎样通过历史生成这些订单。

技术分析:我们如何分析? 技术分析:我们如何分析?

本文简要介绍笔者对于指标重绘,多时间框架指标和日本蜡烛图(K线)的一些观点。本文内容只论及一般特征,不包含编程细节。

使用MetaTrader 5作为MetaTrader 4的信号提供者 使用MetaTrader 5作为MetaTrader 4的信号提供者

分析并通过技术实例说明怎样在MetaTrader 5平台上做交易分析而在MetaTrader 4上做交易。本文将展示如何在您的MetaTrader 5上创建简单的信号提供者,并且把它连接到多个客户端,甚至包括运行MetaTrader 4的客户端。而且您也可以发现怎样在您的真实MetaTrader 4账户中跟随自动交易锦标赛的选手。

如何订购EA交易,并取得预期的结果 如何订购EA交易,并取得预期的结果

如何正确书写规格要求?当订购EA交易或指标时,什么是能从程序员那里期望得到的,什么是不能期望得到的?如何保持一个对话框,要特别注意什么时刻?本文给出这些以及其他许多对很多人来说并不那么显而易见的问题的答案。