MQL5编程基础:文件

2016年11月4日,15:19
德米特里·费多谢夫(Dmitry Fedoseev)
2
30 797

内容

介绍

与许多其他编程语言一样,MQL5具有用于处理文件的功能。尽管在开发MQL5 EA交易程序和指标时,处理文件不是很常见的任务,但是每个开发人员迟早都会面对它。需要处理文件的问题范围足够广泛。它包括生成自定义交易报告,为EA或指标创建带有复杂参数的特殊文件,读取市场数据(例如,新闻日历)等。本文涵盖了在MQL5中使用文件的所有功能。他们每个人都伴随着一个简单的实际任务,旨在磨练您的技能。除任务外,本文还考虑了可以在实践中应用的许多有用功能。

MQL5文档包含文件功能的描述 这里.  

读取文字档

最简单和最常用的功能是读取文本文件。让我们直接练习。打开MetaEditor。选择文件-打开数据文件夹。在新窗口中打开MQL5文件夹。之后,打开“文件”文件夹。该文件夹是一个包含可用于MQL5文件功能处理的文件的文件夹。该限制可确保数据安全。 MetaTrader用户积极共享MQL5应用程序。没有限制,入侵者很容易通过删除或破坏重要文件或窃取个人数据来对您的PC造成损害。

在新打开的MQL5 / Files文件夹中创建一个文本文件。为此,请单击文件夹中的某个位置,然后选择“新建—文本文档”。将文件命名为“ test”。其全名应为“ test.txt”。我建议您在PC上启用文件扩展名的显示。

重新命名文件后,将其打开。它在记事本编辑器中打开。在文件中写入2-3个文本行并保存。确保在“另存为”窗口底部的下拉列表中选择了ANSI编码(图1)。


图1.在Windows记事本中保存一个文本文件。红色箭头显示所选文件的编码 

现在,我们将通过MQL5读取文件。在MetaEditor中创建一个脚本,并将其命名为sTestFileRead。

该文件应在读取或写入之前打开,然后再关闭。该文件由 FileOpen() 具有两个必需参数的函数。第一个是文件名。我们应该在此处指定“ test.txt”。请注意,我们指定的是MQL5 / Files文件夹中的路径,而不是完整路径。第二个参数是 标志 定义使用文件的模式。我们将要读取文件,因此我们应该指定FILE_READ标志。 “ test.txt”是应用ANSI编码的文本文件,这意味着我们应该再使用两个标志:FILE_TXT和FILE_ANSI。通过“或”指示的“或”逻辑运算来组合标志符号。

FileOpen()函数返回文件句柄。我们将不讨论有关句柄功能的详细信息。我们只说它是一个数字值(int),而不是文件的字符串名。打开文件时指定文件的字符串名称,然后使用句柄对该文件执行操作。

让我们打开文件(在sTestFileRead脚本的OnStart()函数中编写代码):

整型 h=FileOpen("test.txt",FILE_READ|FILE_ANSI|FILE_TXT);

之后,请确保该文件实际上已打开。这是通过检查接收到的句柄的值来完成的:

if(h==INVALID_HANDLE){
   Alert("Error opening file");
   return; 
}

文件打开错误很常见。如果文件已经打开,则无法再次打开。该文件可能在某些第三方应用程序中打开。例如,可以同时在Windows记事本和MQL5中打开文件。但是,如果在Microsoft Excel中打开它,则可以在其他任何地方打开它。 

从文本文件读取数据(以FILE_TXT标志打开) is done by the FileReadString() 功能。逐行执行读取。一个函数调用读取单个文件行。让我们阅读一行并在消息框中显示它:

力量ing 力量=FileReadString(h);
Alert(str);

关闭文件:

FileClose(h);

请注意,通过指定FileOpen()函数打开文件时接收到的句柄(h变量)来执行FileReadString()和FileClose()函数的调用。

现在,您可以执行sTestFileRead脚本。如果出现问题,请将您的代码与下面随附的sTestFileRead文件进行比较。脚本操作的结果应显示带有“ test.txt”文件第一行的窗口(图2)。

 
图2。 sTestFileRead 脚本操作结果

到目前为止,我们仅从“ test.txt”文件中读取了一行。为了读取其余两个,我们可以再调用FileReadString()函数两次,但实际上,通常不预先知道文件行数。要解决此问题,我们应该应用 FileIsEnding() 功能 and the 而运营商。如果在读取文件时到达文件末尾,则FileIsEnding()函数将返回“ 真正”。让我们编写一个自定义函数,以使用FileIsEnding()函数读取所有文件行并将其显示在消息框中。对于与文件处理有关的各种教育实验,它可能很有用。我们获得以下功能:

void ReadFileToAlert(力量ing FileName){
   整型 h=FileOpen(FileName,FILE_READ|FILE_ANSI|FILE_TXT);
   if(h==INVALID_HANDLE){
      Alert("Error opening file");
      return;
   }   
   Alert("=== Start ===");   
   while(!FileIsEnding(h)){
      力量ing 力量=FileReadString(h);
      Alert(str);   
   }
   FileClose(h);

 让我们创建sTestFileReadToAlert脚本,将函数复制到它,然后从脚本的OnStart()函数调用它:

void OnStart(){
   ReadFileToAlert("test.txt");
}

出现消息框,其中包含“ ===开始===”行和“ test.txt”文件的所有三行。现在已完全读取文件(图3)。 


图3.我们已经应用了FileIsEnding()函数和“ do while”循环来读取整个文件   

创建一个文本文件

为了创建文件,请使用FileOpen()函数将其打开。使用FILE_READ标志而不是FILE_WRITE一个打开“ test.txt”文件:

整型 h=FileOpen("test.txt",FILE_WRITE|FILE_ANSI|FILE_TXT);

打开文件后,请确保像读取文件时一样检查手柄。如果该函数执行成功,则将创建新的“ test.txt”文件。如果文件已经存在,则将其完全清除。打开要写入的文件时要小心,不要丢失有价值的数据。 

写入文本文件是由 FileWrite() 功能。第一个参数设置文件句柄,而第二个参数设置写入文件的行。在FileWrite()函数的每次调用时都会写入新行。

让我们在循环中将十行写入文件。最终的脚本代码(sTestFileCreate)如下所示:

void OnStart(){
   整型 h=FileOpen("test.txt",FILE_WRITE|FILE_ANSI|FILE_TXT);
   if(h==INVALID_HANDLE){
      Alert("Error opening file");
      return;
   }
   for(整型 i=1;i<=10;i++){
      FileWrite(h,"Line-"+IntegerToString(i));
   }
   FileClose(h);
   Alert("File created");
}

执行代码后,“ test.txt”文件应包含十行。要检查文件内容,请在记事本中打开它或执行sTestFileReadToAlert脚本。

请注意FileWrite()函数。它可能有两个以上的参数。您可以将多个字符串变量传递给该函数,并且在编写时将它们组合为一行。在指定的代码中,FileWrite()函数的调用可以编写如下:

FileWrite(h,"Line-",IntegerToString(i));

写入时,该功能将自动合并行。

写入文本文件的末尾

有时,有必要在现有文件中添加一个或几个新的文本行,而其余内容保持不变。要执行此类操作,应打开文件以同时进行读写。这意味着在调用FileOpen()函数时,应同时指定两个标志(FILE_READ和FILE_WRITE)。

整型 h=FileOpen("test.txt",FILE_READ|FILE_WRITE|FILE_ANSI|FILE_TXT);

如果指定名称的文件不存在,则会创建该文件。如果已经存在,则将其打开,同时其内容保持不变。但是,如果我们立即开始写入文件,则其先前的内容将被删除,因为写入是从文件的开头执行的。

处理文件时,会出现“指针”之类的东西-指示位置的数字值,从该位置开始执行下一个条目或从文件中读取。打开文件时,指针会自动设置在文件的开头。在读取或写入数据期间,它会根据读取或写入的数据大小自动重定位。您可以根据需要自己重新定位指针。为此,请使用  FileSeek() 功能.  

为了保存先前的内容并将新的内容添加到文件的末尾,请在写入之前将指针重定位到文件的末尾:

FileSeek(h,0,SEEK_END);

这三个参数发送到FileSeek()函数:句柄,指针重定位值和从中计算移位的位置。在此示例中,SEEK_END常量表示文件的结尾。因此,指针从文件末尾移了0个字节(意味着移到其末尾)。

添加到文件中的最终脚本代码如下:

void OnStart(){
   整型 h=FileOpen("test.txt",FILE_READ|FILE_WRITE|FILE_ANSI|FILE_TXT);
   if(h==INVALID_HANDLE){
      Alert("Error opening file");
      return;
   }
   FileSeek(h,0,SEEK_END);
   FileWrite(h,"Additional line");
   FileClose(h);
   Alert("Added to file");
}

该脚本也附在下面(sTestFileAddToFile)。启动脚本并检查test.txt文件的内容。每次调用sTestFileAddToFile脚本都会向test.txt添加一行。

更改文本文件的指定行

对于文本文件,在整个文件中自由移动指针的功能只能用于添加文件,不适用于进行更改。由于文件行只是一个概念,因为文件实际上包含连续的一系列数据,因此无法对某个文件行进行更改。有时,此类系列包含在文本编辑器中不可见的特殊字符。他们指出,以下信息应换行显示。如果我们将指针设置在行的开头并开始写入,则如果写入数据的大小小于现有行,则行中的先前数据将保留。否则,新的线符号将与下一部分线数据一起删除。

让我们尝试替换test.txt中的第二行。打开文件进行读取和写入,读取一行以将指针重新定位到第二行的开头,并写入由两个字母“ AB”组成的新行(下面附有sTestFileChangeLine2-1脚本):

void OnStart(){
   整型 h=FileOpen("test.txt",FILE_READ|FILE_WRITE|FILE_ANSI|FILE_TXT);
   if(h==INVALID_HANDLE){
      Alert("Error opening file");
      return;
   }
   力量ing 力量=FileReadString(h);
   FileWrite(h,"AB");
   FileClose(h);
   Alert("Done");
} 

现在,获得的test.txt文件如下所示(图4):

 
图4.尝试更改一行后的文本文件的内容 

现在,我们有两行而不是一行:“ AB”和“ -2”。 “ -2”是第二行剩下的内容,其中删除了四个字符。原因是当使用FileWrite()函数编写一行时,它将新的行符号添加到所写文本的末尾。在Windows操作系统中,换行符号由两个字符组成。如果在“ AB”行中向他们添加两个字符,我们可以理解为什么在结果文件中删除了四个字符。 

执行sTestFileCreate脚本以还原test.txt,然后尝试将第二行替换为更长的行。让我们编写“ Line-12345”(下面随附的sTestFileChangeLine2-2脚本):

void OnStart(){
   整型 h=FileOpen("test.txt",FILE_READ|FILE_WRITE|FILE_ANSI|FILE_TXT);
   if(h==INVALID_HANDLE){
      Alert("Error opening file");
      return;
   }
   力量ing 力量=FileReadString(h);
   FileWrite(h,"Line-12345");
   FileClose(h);
   Alert("Done");
}

让我们看一下结果文件(图5):

 
图5.第二次尝试更改文本文件单行的结果 

由于新行比上一行长,因此第三行也受到了影响。 

更改文本文件的唯一方法是完整读取和重写它们。我们应该将文件读取到数组,更改必要的数组元素,将其逐行保存到另一个文件,删除旧文件并重命名新文件。有时,不需要数组:从一个文件读取行时,可以将它们写入另一个文件。在某个时间点,将进行更改并将其保存在必要的行中。之后,将删除旧文件,然后将新文件重命名。

让我们应用后一个选项(不更改数组即可更改)。首先,我们应该创建一个临时文件。让我们编写一个用于接收临时文件的唯一名称的函数。文件名和扩展名将传递给该函数。检查文件是否存在(按标准 FileIsExists() 函数)。如果文件存在,则会向其添加一个数字,直到没有检测到具有该名称的文件为止。该函数如下所示:

力量ing TmpFileName(力量ing Name,力量ing Ext){
   力量ing fn=Name+"."+Ext; // forming name
   整型 n=0;
   while(FileIsExist(fn)){ // if the file exists
      n++;
      fn=Name+IntegerToString(n)+"."+Ext; // add a number to the name
   }
   return(fn);
}

让我们创建sTestFileChangeLine2-3脚本,将函数复制到脚本中,并将以下代码放入OnStart()函数中。

打开test.txt以阅读:

整型 h=FileOpen("test.txt",FILE_READ|FILE_ANSI|FILE_TXT);

接收临时文件的名称并打开它:

力量ing tmpName=TmpFileName("test","txt");

整型 tmph=FileOpen(tmpName,FILE_WRITE|FILE_ANSI|FILE_TXT);

逐行读取文件。所有读取的行都发送到临时文件,第二行被替换:

   整型 cnt=0;
   while(!FileIsEnding(h)){
      cnt++;
      力量ing 力量=FileReadString(h);
      if(cnt==2){
         // replace the line
         FileWrite(tmph,"New line-2");
      }
      else{
         // rewrite the line with no changes
         FileWrite(tmph,str);
      }
   }

关闭两个文件:

FileClose(tmph);
FileClose(h);

现在,我们要做的就是删除原始文件并重命名临时文件。标准 FileDelete() 功能用于删除。

FileDelete("test.txt");

要重命名文件,我们应该使用标准 FileMove() 用于移动或重命名文件的功能。该函数接收四个必填参数:重定位文件(源文件)的名称,文件位置标志,新文件名(目标标志),覆盖标志。文件名应该很清楚,所以现在是时候仔细看看第二和第四参数-标志。第二个参数定义源文件的位置。 MQL5中可用于处理的文件不仅可以位于终端的MQL5 / Files文件夹中,还可以位于所有终端的公共文件夹中。我们将在以后更详细地考虑这一点。现在,让我们将其设置为0。最后一个参数定义目标文件的位置。如果目标文件存在,它可能还具有定义操作的附加标志。由于我们已经删除了源文件(目标文件),因此第四个参数设置为0:

FileMove(tmpName,0,"test.txt",0);

在执行sTestFileChangeLine2-3脚本之前,请使用sTestFileCreate脚本还原test.txt。在sTestFileChangeLine2-3脚本操作之后,text.txt应具有以下内容(图6):

 
图6.替换行后的文件内容

让我们返回FileMove()函数。如果我们将FILE_REWRITE标志(允许我们重写目标文件)设置为第四个参数:

FileMove(tmpName,0,"test.txt",FILE_REWRITE);

不必从脚本中删除源文件。此选项在下面随附的sTestFileChangeLine2-3脚本中使用。 

代替FileMove()函数,我们可以使用另一个标准函数 FileCopy(),但是在这种情况下,我们需要删除临时文件:

FileCopy(tmpName,0,"test.txt",FILE_REWRITE);
FileDelete(tmpName); 

将文本文件读取到数组

本文已经描述了一种有用的功能(接收到未占用的文件名)。现在,让我们开发在处理文件时经常使用的另一个功能-将文件读取到数组。文件名和行数组将传递给函数。该数组通过链接传递,并在函数中填充文件内容。函数返回 真假 取决于其运行结果。 

bool ReadFileToArray(力量ing FileName,力量ing & Lines[]){
   ResetLastError();
   整型 h=FileOpen(FileName,FILE_READ|FILE_ANSI|FILE_TXT);
   if(h==INVALID_HANDLE){
      整型 ErrNum=GetLastError();
      printf("Error opening file %s # %i",FileName,ErrNum);
      return();
   }
   整型 cnt=0; // use the variable to count the number of file lines
   while(!FileIsEnding(h)){
      力量ing 力量=FileReadString(h); // read the next line from the file
      // remove spaces to the left 和 to the right to detect 和 avoid using empty lines
      StringTrimLeft(str); 
      StringTrimRight(str);
      if(str!=""){ 
         if(cnt>=ArraySize(Lines)){ // array filled completely
            ArrayResize(Lines,ArraySize(Lines)+1024); // increase the array size by 1024 elements
         }
         Lines[cnt]=str; // send the read line to the array
         cnt++; // increase the counter of read lines
      }
   }
   ArrayResize(Lines,cnt);
   FileClose(h);
   return(真正);
}

我们不会详细考虑此功能,因为到目前为止,从此处提供的数据中应该已经清楚了所有功能。此外,它已被详细评论。我们只应提及一些细微差别。从文件读取行到 力量 变量,行边缘的空格将被删除 StringTrimLeft()StringTrimRight() 职能。然后,检查是否 力量 字符串不为空。这样做是为了跳过不必要的空行。填充数组后,它以块为单位增加1024个元素,而不是单个元素。该功能以这种方式更快地工作。最后,根据读取行的实际数量缩放数组。

该函数可以在下面附带的sTestFileReadFileToArray脚本中找到。

用分隔符创建文本文件

到目前为止,我们仅考虑了简单的文本文件。但是,还有另一种文本文件-带分隔符的文本文件。通常,它们具有.csv扩展名(“逗号分隔值”的缩写)。实际上,这些是纯文本文件,可以在文本编辑器中打开它们,也可以手动读取和编辑它们。某个字符(不一定是逗号)用作行中的字段分隔符。因此,与简单文本文件相比,您可以对它们执行一些不同的操作。主要区别在于,在一个简单的文本文件中,当调用FileRedaString()函数时,将读取整行,而在具有分隔符的文件中,将执行读取直到分隔符或行尾。 FileWrite()函数的工作方式也有所不同:函数中枚举的所有已写入变量都不能简单地连接到一行。而是在它们之间添加了分隔符。 

让我们尝试创建一个csv文件。打开文本文件,就像我们已经完成的写操作一样,指定FILE_CSV标志而不是FILE_TXT标志。第三个参数是用作分隔符的符号:

整型 h=FileOpen("test.csv",FILE_WRITE|FILE_ANSI|FILE_CSV,";");

让我们将十行写入文件,每行三个字段:

   for(整型 i=1;i<=10;i++){
      力量ing 力量="Line-"+IntegerToString(i)+"-";
      FileWrite(h,str+"1",str+"2",str+"3");
   }

确保最后关闭文件。可以在下面随附的sTestFileCreateCSV脚本中找到该代码。结果将创建“ test.csv”文件。文件内容如图7所示。正如我们所看到的,FileWrite()函数参数现在形成一行并在它们之间使用分隔符。

 
图7.带分隔符的文件的内容

读取带分隔符的文本文件

现在,让我们尝试以与本文开头的文本文件相同的方式读取csv文件。让我们复制名为sTestFileReadToAlertCSV的sTestFileReadToAlert脚本。更改ReadFileToAlert()函数中的第一个字符串:  

整型 h=FileOpen(FileName,FILE_READ|FILE_ANSI|FILE_CSV,";");

将ReadFileToAlert()函数重命名为ReadFileToAlertCSV()并更改传递给该函数的文件的名称:

void OnStart(){
   ReadFileToAlertCSV("test.csv");
}

脚本操作结果显示文件已被一个字段读取。确定何时读取一行的字段并开始新行将是很好的选择。的 FileIsLineEnding() 功能适用于此。

让我们复制名为sTestFileReadToAlertCSV2的sTestFileReadToAlertCSV脚本,将ReadFileToAlertCSV函数重命名为ReadFileToAlertCSV2并进行更改。添加FileIsLineEnding()函数:如果返回'true',则显示分隔线“ ---”。 

void ReadFileToAlertCSV2(力量ing FileName){
   整型 h=FileOpen(FileName,FILE_READ|FILE_ANSI|FILE_CSV,";");
   if(h==INVALID_HANDLE){
      Alert("Error opening file");
      return;
   }   
   Alert("=== Start ===");   
   while(!FileIsEnding(h)){
      力量ing 力量=FileReadString(h);
      Alert(str);
      if(FileIsLineEnding(h)){
         Alert("---");
      }
   }
   FileClose(h);
}

 现在,脚本将发送到消息窗口的字段分为几组(图8)。


图8.单个文件行的字段组之间的“-”分隔符 

使用分隔符读取文件到数组

现在,我们已经熟悉了使用csv文件的方法,让我们开发另一个有用的功能,将csv文件读取到数组中。读取结构数组,每个元素对应一个文件行。该结构将包含一个线阵列,其每个元素都对应一个线字段。 

结构体:

力量uct SLine{
   力量ing line[];
};  

功能:

bool ReadFileToArrayCSV(力量ing FileName,SLine & Lines[]){
   ResetLastError();
   整型 h=FileOpen(FileName,FILE_READ|FILE_ANSI|FILE_CSV,";");
   if(h==INVALID_HANDLE){
      整型 ErrNum=GetLastError();
      printf("Error opening file %s # %i",FileName,ErrNum);
      return();
   }   
   整型 lcnt=0; // variable for calculating lines 
   整型 fcnt=0; // variable for calculating line fields    
   while(!FileIsEnding(h)){
      力量ing 力量=FileReadString(h);
      // new line (new element of the 结构体 array)
      if(lcnt>=ArraySize(Lines)){ // 结构体 array completely filled
         ArrayResize(Lines,ArraySize(Lines)+1024); // increase the array size by 1024 elements
      }
      ArrayResize(Lines[lcnt].field,64);// change the array size in the 结构体
      Lines[lcnt].field[0]=str; // assign the first field value
      // start reading other fields in the line
      fcnt=1; // till 上e element in the line array is occupied
         while(!FileIsLineEnding(h)){ // read the rest of fields in the line
            str=FileReadString(h);
            if(fcnt>=ArraySize(Lines[lcnt].field)){ // field array is completely filled
               ArrayResize(Lines[lcnt].field,ArraySize(Lines[lcnt].field)+64); // increase the array size by 64 elements
            }     
            Lines[lcnt].field[fcnt]=str; // assign the value of the next field
            fcnt++; // increase the line counter
         }
      ArrayResize(Lines[lcnt].field,fcnt); // change the size of the field array according to the actual number of fields
      lcnt++; // increase the line counter
   }
   ArrayResize(Lines,lcnt); // change the array of 结构体s (lines) according to the actual number of lines
   FileClose(h);
   return(真正);
}

我们不会仅在最关键的地方详细讨论此功能。在while(!FileIsEnding(h))循环的开头读取一个字段。在这里,我们找到了要添加到结构数组中的元素。检查数组大小,并在必要时增加1024个元素。一次更改字段数组的大小。立即为其设置64个元素的大小,将从文件中读取的第一行字段的值分配给索引为0的元素。之后,读取while(!FileIsLineEnding(h))循环中的其余字段。读取另一个字段后,检查数组的大小并在必要时增加它的大小,并将从文件读取的行发送到数组。读完该行到最后(退出 while(!FileIsLineEnding(h)) 循环),请根据其实际数量更改字段数组的大小。最后,根据实际读取的行数调整行数组的大小。 

该函数可以在下面附带的sTestFileReadFileToArrayCSV脚本中找到。该脚本将test.csv文件读取到阵列,并在消息窗口中显示该阵列。结果与图8所示的结果相同。 

使用分隔符将数组写入文本文件

如果预先知道行中的字段数,则任务非常简单。在“使用分隔符创建文本文件”部分中,已经解决了类似的任务。如果字段数未知,则可以将所有字段收集到循环中且带有分隔符的单行中,然后将该行写入使用FILE_TXT标志打开的文件中。

打开文件: 

整型 h=FileOpen("test.csv",FILE_WRITE|FILE_ANSI|FILE_TXT);

使用分隔符将所有字段(数组元素)收集到一行中。该行的末尾不应有分隔符,否则该行中将有一个多余的空字段:

   力量ing 力量="";
   整型 size=ArraySize(a);
   if(size>0){
      str=a[0];
      for(整型 i=1;i<size;i++){
         力量=str+";"+a[i]; // merge fields using a separator 
      }
   }

将行写入文件并关闭它: 

FileWriteString(h,str);
FileClose(h);

该示例可以在下面随附的sTestFileWriteArrayToFileCSV脚本中找到。

UNICODE文件

到目前为止,打开文件以定义其编码时始终指定FILE_ANSI标志。在这种编码中,一个字符对应一个字节,因此,整个字符集限制为256个符号。但是,如今,UNICODE编码已被广泛使用。在这种编码中,一个符号由几个字节定义,并且一个文本文件可能包含大量字符,包括来自不同字母的字母,象形文字和其他图形符号。

让我们进行一些实验。在编辑器中打开sTestFileReadToAlert脚本,将其保存在sTestFileReadToAlertUTF名称下,然后用FILE_UNICODE替换FILE_ANSI标志:

整型 h=FileOpen(FileName,FILE_READ|FILE_UNICODE|FILE_TXT);

由于test.txt保存在ANSI中,所以新窗口包含乱码文本(图9)。

  
图9.当文件的原始编码与打开文件时指定的原始编码不匹配时可以看到的乱码

显然,发生这种情况是因为文件的原始编码与打开文件时指定的编码不匹配。

在编辑器中打开sTestFileCreate脚本,将其保存在sTestFileCreateUTF名称下,然后用FILE_UNICODE替换FILE_ANSI标志:

整型 h=FileOpen("test.txt",FILE_WRITE|FILE_UNICODE|FILE_TXT);

启动sTestFileCreateUTF脚本以创建一个新的test.txt文件。现在,sTestFileReadToAlertUTF显示可读文本(图10)。

 
图10.使用 sTestFileReadToAlertUTF script 读取由 sTestFileCreateUTF 脚本

在记事本中打开test.txt并在主菜单中执行“另存为...”命令。请注意,在“另存为”窗口底部的“编码”列表中选择了Unicode。记事本以某种方式定义了文件编码。 Unicode文件以标准符号集开始,即所谓的BOM(字节顺序标记)。稍后,我们将回到这一点并编写用于定义文本文件类型(ANSI或UNCODE)的函数。 

使用带有分隔符的文本文件的附加功能

从各种 file 功能s 为了处理文本文件的内容(简单文件和带分隔符的文件),我们实际上只需要两个文件: FileWrite() and FileReadString()。除其他事项外,FileReadString()函数还用于处理二进制文件(下面将详细介绍)。除FileWrite()函数外,还可以使用FileWriteString()函数,尽管这并不重要。 

当使用带有分隔符的文本文件时,可以使用其他一些功能,使工作更加方便: FileReadBool()FileReadNumber() and FileReadDatetime(). FileReadNumber()函数用于读取数字。如果我们事先知道从文件读取的字段仅包含数字,则可以应用此功能。其效果等同于使用FileReadString()函数读取一行并将其转换为数字。 StringToDouble() 功能。同样,FileReadBool()函数用于读取布尔类型的值。该字符串可能包含 真假 要么 0/1。 FileReadDatetime()函数用于读取行格式的数据,并将其转换为数字datetime-type值。其效果类似于读取一行并使用 StringToTime() 功能.  

二进制文件

前面讨论的文本文件相当方便,因为通过程序方式读取的文本文件的内容与在文本编辑器中打开文件时看到的内容一致。您可以通过在编辑器中检查文件来轻松管理程序操作结果。如有必要,可以手动更改文件。文本文件的缺点包括使用它们时选项有限(如果我们想到替换单个文件行时遇到的困难,这是显而易见的)。

如果文本文件很小,则使用起来很舒服。但是,它的大小越大,使用它所需的时间就越多。如果需要快速处理大量数据,请使用二进制文件。

以二进制模式打开文件时,将指定FILE_BIN标志而不是FILE_TXT或FILE_CSV。指定FILE_ANSI或FILE_UNCODE编码文件没有意义,因为二进制文件是带有数字的文件。

当然,我们可以在记事本文本编辑器中查看二进制文件的内容。有时,我们甚至可以在其中看到字母和可读的文本,但这更多与记事本本身而不是文件内容有关。

无论如何,您绝对不应该在文本编辑器中编辑该文件,因为在此过程中该文件会损坏。我们不会详细说明原因,我们只接受这个事实。当然,有特殊的二进制文件编辑器,但是编辑过程仍然不直观。

二进制文件,变量

在MQL5中使用文件的大多数功能都是为二进制模式设计的。有用于读取/写入不同类型变量的函数:  

FileReadDouble() FileWriteDouble()
FileReadFloat() FileWriteFloat()
FileReadInteger() FileWriteInteger()
FileReadLong() FileWriteLong()
FileReadString() FileWriteString()
FileReadStruct() FileWriteStruct()

在此我们将不描述所有可变的读写功能。我们只需要其中之一,而其余所有都以相同的方式使用。让我们尝试使用FileWriteDouble()和FileReadDouble()函数。

首先,创建一个文件,向其中写入三个变量,并以随机顺序读取它们。 

打开文件:

整型 h=FileOpen("test.bin",FILE_WRITE|FILE_BIN);

将三个双精度变量值分别为1.2、3.45和6.789写入文件:

FileWriteDouble(h,1.2);
FileWriteDouble(h,3.45);
FileWriteDouble(h,6.789);

不要忘记关闭文件。

可以在随附的sTestFileCreateBin脚本中找到该代码。结果,test.bin文件出现在MQL5 / Files文件夹中。在记事本中查看其内容(图11)。打开记事本并将文件拖到其中:

 
图11.记事本中的二进制文件

如我们所见,在记事本中查看此类文件毫无意义。

现在,让我们阅读文件。显然,应该使用FileReadDouble()函数进行读取。打开文件:

整型 h=FileOpen("test.bin",FILE_READ|FILE_BIN);

声明三个变量,从文件中读取它们的值,然后在消息框中显示它们:

 v1,v2,v3;
   
v1=FileReadDouble(h);
v2=FileReadDouble(h);
v3=FileReadDouble(h);
   
Alert(DoubleToString(v1)," ",DoubleToString(v2)," ",DoubleToString(v3));
  

不要忘记关闭文件。可以在随附的sTestFileReadBin脚本中找到该代码。结果,我们收到以下消息:1.20000000 3.45000000 6.78900000。

知道二进制文件的结构,可以对它们进行一些有限的更改。让我们尝试更改第二个变量,而不用重写整个文件。

打开文件:

整型 h=FileOpen("test.bin",FILE_READ|FILE_WRITE|FILE_BIN);

打开后,将指针移到指定位置。建议使用sizeof()函数进行位置计算。它返回指定数据类型的大小。熟悉 资料类型 及其大小。将指针移到第二个变量的开头:

FileSeek(h,sizeof()*1,0);

为了更清楚,我们实现了sizeof(double)* 1乘法,因此很明显这是第一个变量的结尾。如果需要更改第三个变量,则需要乘以2。

写下新值: 

FileWriteDouble(h,12345.6789);

可以在随附的sTestFileChangeBin脚本中找到该代码。执行脚本后,启动sTestFileReadBin脚本并接收: 1.20000000 12345.67890000 6.78900000。

您可以用相同的方式读取某个变量(而不是整个文件)。让我们阅读一下从test.bin中读取第三个double变量的代码。

打开文件:

整型 h=FileOpen("test.bin",FILE_READ|FILE_BIN);

移动指针,读取值并将其显示在消息框中:

FileSeek(h,sizeof()*2,SEEK_SET);
 v=FileReadDouble(h);
Alert(DoubleToString(v));

可以在下面附带的sTestFileReadBin2脚本中找到此示例。结果,我们收到以下消息:6.78900000-第三个变量。更改代码以读取第二个变量。

您可以以相同的方式保存和读取其他类型的变量及其组合。重要的是要知道文件结构,以正确计算指针设置位置。 

二进制文件,结构

如果您需要将多个不同类型的变量写入文件,则描述结构并读取/写入整个结构要比逐个读取/写入变量要方便得多。文件通常以描述文件中数据位置(文件格式)的结构开头,后跟数据。但是,存在一个局限性:结构不应该具有动态数组和行,因为它们的大小未知。

让我们尝试写入和读取文件的结构。用几个不同类型的变量描述结构:

力量uct STest{
    ValLong;
    VarDouble;
   整型 ArrInt[3];
   bool VarBool;
};

可以在随附的sTestFileWriteStructBin脚本中找到该代码。声明两个变量,并在OnStart()函数中用不同的值填充它们:

STest s1;
STest s2;
   
s1.ArrInt[0]=1;
s1.ArrInt[1]=2; 
s1.ArrInt[2]=3;
s1.ValLong=12345;
s1.VarDouble=12.34;
s1.VarBool=真正;
         
s2.ArrInt[0]=11;
s2.ArrInt[1]=22; 
s2.ArrInt[2]=33;
s2.ValLong=6789;
s2.VarDouble=56.78;
s2.VarBool=;  

现在,打开文件:

整型 h=FileOpen("test.bin",FILE_WRITE|FILE_BIN);

将两个结构都写入其中:

FileWriteStruct(h,s1);
FileWriteStruct(h,s2); 

不要忘记关闭文件。执行脚本以创建文件。

现在,让我们阅读文件。阅读第二个结构。

打开文件:

整型 h=FileOpen("test.bin",FILE_READ|FILE_BIN);

将指针移到第二个结构的开头:

FileSeek(h,sizeof(STest)*1,SEEK_SET);

声明变量(将STest结构描述添加到文件的开头),然后从文件读取数据:

STest s;
FileReadStruct(h,s);

在窗口中描述结构字段的值:

Alert(s.ArrInt[0]," ",s.ArrInt[1]," ",s.ArrInt[2]," ",s.ValLong," ",s.VarBool," ",s.VarDouble);   

结果,我们将在消息框中看到以下行:11 22 33 6789 假 56.78。该线对应于第二结构数据。

该示例的代码可在下面随附的sTestFileReadStructBin脚本中找到。

通过变量编写结构

在MQL5中, 结构体 字段彼此跟随而没有移位(对齐),因此可以读取某些结构字段而没有任何困难。

从test.bin文件的第二个结构中读取double变量的值。计算设置指针的位置很重要: 

FileSeek(h,sizeof(STest)+sizeof(),SEEK_SET);

其余与我们在本文中已经做过很多次的相似:打开文件,读取,关闭。该示例的代码可在下面随附的sTestFileReadStructBin2脚本中找到。

定义UNICODE文件,FileReadInteger函数

在稍微熟悉二进制文件之后,我们可以创建一个有用的函数来定义UNICODE文件。这些文件可以通过等于255的初始字节的值来区分。代码255对应于不可打印的符号,因此它不能出现在普通的ANSI文件中。

这意味着我们应该从文件中读取一个字节并检查其值。的 FileReadInteger() 函数用于读取各种整数变量,除了 ,因为它接收到指定读取变量大小的参数。读一个字节到 v 文件中的变量:

uchar v=FileReadInteger(h,CHAR_VALUE);

现在,我们只需要检查变量值。完整的功能代码如下所示:

bool CheckUnicode(力量ing FileName,bool & Unicode){
   ResetLastError();
   整型 h=FileOpen(FileName,FILE_READ|FILE_BIN);
   if(h==INVALID_HANDLE){
      整型 ErrNum=GetLastError();
      printf("Error opening file %s # %i",FileName,ErrNum);
      return();
   }
   uchar v=FileReadInteger(h,CHAR_VALUE);
   Unicode=(v==255);
   FileClose(h);
   return(真正);
}

函数返回 真假 取决于检查是否成功。文件名作为第一个参数传递给函数,而第二个(通过链接传递)包含等于 真正 用于UNICODE文件和 函数执行后用于ANSI文件。 

函数代码及其调用示例可在下面随附的sTestFileCheckUnicode脚本中找到。启动sTestFileCreate脚本,并使用sTestFileCheckUnicode脚本检查其类型。之后,启动sTestFileCreateUTF脚本并再次运行sTestFileCheckUnicode脚本。您将获得不同的结果。 

二进制文件,数组,结构数组

当处理大量数据时,二进制文件的主要优点变得显而易见。数据通常位于数组(因为很难使用单独的变量来接收大量数据)和字符串中。数组可以包含标准变量和应满足上述要求的结构。它们不应包含动态数组和字符串。

使用以下命令将数组写入文件 FileWriteArray() 功能。文件句柄作为第一个参数传递给函数,后跟数组名称。以下两个参数是可选的。如果不需要保存整个数组,请指定数组的初始元素索引和保存的元素数。 

数组使用 FileReadArray() 函数,函数参数与FileWriteArray()函数参数相同。

让我们来写 整型 该变量由文件的三个元素组成: 

void OnStart(){
   整型 h=FileOpen("test.bin",FILE_WRITE|FILE_BIN);
   if(h==INVALID_HANDLE){
      Alert("Error opening file");
      return;
   }   
   整型 a[]={1,2,3};   
   FileWriteArray(h,a);   
   FileClose(h);
   Alert("File written");
}

可以在下面随附的sTestFileWriteArray文件中找到该代码。

现在,阅读(sTestFileReadArray脚本)并将其显示在窗口中:

void OnStart(){
   整型 h=FileOpen("test.bin",FILE_READ|FILE_BIN);
   if(h==INVALID_HANDLE){
      Alert("Error opening file");
      return;
   }   
   整型 a[];   
   FileReadArray(h,a);   
   FileClose(h);
   Alert(a[0]," ",a[1]," ",a[2]);   
}

结果,我们获得了与先前指定的数组相对应的“ 1 2 3”行。请注意,未定义数组大小,并且在调用FileReadArray()函数时未指定数组大小。而是读取了整个文件。但是文件可能具有多个不同类型的数组。因此,保存文件大小也是合理的。让我们写 整型 文件的数组都以 整型 包含其大小的变量:

void OnStart(){
   整型 h=FileOpen("test.bin",FILE_WRITE|FILE_BIN);
   if(h==INVALID_HANDLE){
      Alert("Error opening file");
      return;
   }   
   
   // two arrays
   整型 a1[]={1,2,3}; 
    a2[]={1.2,3.4};
   
   // define the size of the arrays
   整型 s1=ArraySize(a1);
   整型 s2=ArraySize(a2);
   
   // write the array 1
   FileWriteInteger(h,s1,INT_VALUE); // write the array size
   FileWriteArray(h,a1); // write the array
   
   // write the array 2
   FileWriteInteger(h,s2,INT_VALUE); // write the array size
   FileWriteArray(h,a2); // write the array   
      
   FileClose(h);
   Alert("File written");
}

可以在下面随附的sTestFileWriteArray2脚本中找到该代码。 

现在,在读取文件时,我们先读取数组大小,然后再将指定数量的元素读取到数组中:

void OnStart(){
   整型 h=FileOpen("test.bin",FILE_READ|FILE_BIN);
   if(h==INVALID_HANDLE){
      Alert("Error opening file");
      return;
   }   
   整型 a1[];
    a2[];
   整型 s1,s2;
   
   s1=FileReadInteger(h,INT_VALUE); // read the size of the array 1
   FileReadArray(h,a1,0,s1); // read the number of elements set in s1 to the array 
   
   s2=FileReadInteger(h,INT_VALUE); // read the size of the array 2
   FileReadArray(h,a2,0,s2); // read the number of elements set in s2 to the array    

   FileClose(h);
   Alert(ArraySize(a1),": ",a1[0]," ",a1[1]," ",a1[2]," :: ",ArraySize(a2),": ",a2[0]," ",a2[1]);   
}

可以在下面随附的sTestFileReadArray2脚本中找到该代码。

结果,脚本显示了以下消息:3:1 2 3-2:1.2 3.4与写入文件的先前数组的大小和内容相对应。

使用FileReadArray()函数读取数组时,将自动缩放数组。但是,仅当当前大小小于读取元素的数量时才执行缩放。如果数组大小超过数字,则保持不变。而是仅填充数组的一部分。

使用结构数组与使用标准类型的数组完全相同,因为结构大小已正确定义(没有动态数组和字符串)。在此我们将不提供包含结构数组的示例。您可以自己尝试。

另外,请注意,由于我们能够在整个文件中移动指针,因此可以仅读取数组元素之一或数组的一部分。正确计算将来的指针位置很重要。为了缩短文章的长度,此处也没有显示读取单独元素的示例。您也可以自己尝试。

二进制文件,字符串,行数组

FileWriteString() 函数用于将字符串写入二进制文件。两个强制性参数传递给该函数:文件句柄和写入文件的一行。第一个参数是可选的:如果只应写一行的一部分,则可以设置写符号的数量。 

该行由 FileReadString() 功能。在此函数中,第一个参数是一个句柄,而第二个(可选)参数用于设置读取字符的数量。

通常,写入/读取行与使用数组非常相似:一行与整个数组相似,而一个行字符与单个数组元素有很多共同之处,因此我们将不显示单个示例线写作/阅读。相反,我们将考虑更复杂的示例:写入和读取字符串数组。首先,让我们将 整型 具有数组大小的变量,然后在循环中编写单独的元素,并添加 整型 变量的大小在每个变量的开头: 

void OnStart(){
   整型 h=FileOpen("test.bin",FILE_WRITE|FILE_BIN);
   if(h==INVALID_HANDLE){
      Alert("Error opening file");
      return;
   }   
   
   力量ing a[]={"Line-1","Line-2","Line-3"}; // written array 

   FileWriteInteger(h,ArraySize(a),INT_VALUE); // write the array size
   
   for(整型 i=0;i<ArraySize(a);i++){
      FileWriteInteger(h,StringLen(a[i]),INT_VALUE); // write the line size (a single array element)
      FileWriteString(h,a[i]);
   }

   FileClose(h);
   Alert("File written");
}

可以在随附的sTestFileWriteStringArray脚本中找到该代码。

读取时,请先读取数组大小,然后更改其大小并读取单独的元素以读取其大小:

void OnStart(){
   整型 h=FileOpen("test.bin",FILE_READ|FILE_BIN);
   if(h==INVALID_HANDLE){
      Alert("Error opening file");
      return;
   }   
   
   力量ing a[]; // read the file to this array
   
   整型 s=FileReadInteger(h,INT_VALUE); // read the array size
   ArrayResize(a,s); // change the array size
   
   for(整型 i=0;i<s;i++){ // by all array elements
      整型 ss=FileReadInteger(h,INT_VALUE); // read the line size
      a[i]=FileReadString(h,ss); // read the line
   }

   FileClose(h);

   // display the read array
   Alert("=== Start ===");
   for(整型 i=0;i<ArraySize(a);i++){
      Alert(a[i]);
   }

}

可以在随附的sTestFileReadStringArray脚本中找到该代码。 

文件共享文件夹

到目前为止,我们已经处理了MQL5 / Files目录中的文件。但是,这不是可以定位文件的唯一位置。在MetaEditor的主菜单中,执行文件-打开公共数据文件夹。带有文件目录的文件夹将打开。它还可能包含从MQL5开发的应用程序中可用的文件。请注意它的路径(图12):


图12.公共数据文件夹的路径 

通用数据文件夹的路径与本文中所涉及的终端和Files目录的路径无关。无论您启动了多少个终端(包括使用“ / portable”键运行的终端),都将向所有终端打开相同的共享文件夹。

文件夹的路径可以通过编程定义。数据文件夹的路径(包含整篇文章中使用的MQL5 / Files目录):

TerminalInfoString(TERMINAL_DATA_PATH);

共享数据文件夹(包含“文件”目录)的路径:

TerminalInfoString(TERMINAL_COMMONDATA_PATH);

同样,您可以定义终端的路径(终端安装的根目录):

TerminalInfoString(TERMINAL_PATH);

与MQL5 / Files目录类似,使用共享文件夹中的文件时,无需指定完整路径。相反,您只需要将FILE_COMMON标志添加到传递给FileOpen()函数的标志的组合中即可。某些文件功能具有用于指定共享文件夹标志的特定参数。这些是 FileDelete(), FileMove(), FileCopy() 和其他一些。

将test.txt从MQL5 / Files文件夹复制到common data文件夹:

   if(FileCopy("test.txt",0,"test.txt",FILE_COMMON)){
      Alert("File copied");
   }
   else{
      Alert("Error copying file");
   }

可以在随附的sTestFileCopy脚本中找到该代码。执行脚本后,test.txt文件出现在共享的Files文件夹中。如果我们第二次启动脚本,我们将收到错误消息。为了避免这种情况,请通过添加FILE_REWRITE标志来允许文件覆盖:

FileCopy("test.txt",0,"test.txt",FILE_COMMON|FILE_REWRITE)

现在,将文件从共享文件夹复制到具有不同名称的相同文件夹(sTestFileCopy2脚本):

FileCopy("test.txt",FILE_COMMON,"test_copy.txt",FILE_COMMON)

最后,将文件从通用文件夹复制到MQL5 / Files(sTestFileCopy3脚本):

FileCopy("test.txt",FILE_COMMON,"test_copy.txt",0)

尽管未创建副本,但FileMove()函数的调用方式相同。而是,文件被移动(或重命名)。

测试仪中的文件

到目前为止,我们的文件处理仅与在帐户(在图表上启动)上运行的MQL5程序(脚本,EA,指标)有关。但是,在测试器中启动EA时,一切都不同。 MetaTrader 5测试人员可以使用远程代理执行分布式(云)测试。粗略地说,在一台PC上执行优化运行1-10(编号是有条件的),而在另一台PC上执行优化11-20,等等。这可能会造成困难并影响文件的使用。让我们考虑这些功能,并形成在测试仪中使用文件时应遵循的原则。

处理文件时,FileOpen()函数访问位于终端数据文件夹内MQL5 / Files目录中的文件。在测试时,该函数访问测试代理文件夹中的MQL5 / Files目录文件。例如,如果在单次优化运行(或单次测试)过程中需要这些文件,以存储头寸或挂单上的数据,那么您要做的就是在下一次运行之前(初始化EA时)清除文件。如果该文件是手动生成的,并且也将用于确定任何EA操作参数,则它将位于终端数据文件夹的MQL5 / Files目录中。这意味着测试人员将无法看到它。为了让EA访问文件,应将其传递给代理。这是通过设置“ #property tester_file”属性在EA中完成的。因此,可以发送任意数量的文件:

#property tester_file "file1.txt"
#property tester_file "file2.txt"
#property tester_file "file3.txt"

但是,即使使用“ #property tester_file”指定了文件,EA仍会写入位于测试代理目录中的文件副本。终端数据文件夹中的文件保持不变。 EA从代理文件夹中进一步读取文件。换句话说,读取更改的文件。因此,如果您需要保存一些数据以在EA测试和优化过程中进行进一步分析,则将数据保存到文件不适用。你应该用 镜框 代替。

如果不使用远程代理,请使用共享文件夹中的文件(打开文件时设置FILE_COMMON标志)。在这种情况下,无需在EA属性中指定文件名,并且EA能够写入文件。简而言之,使用公用数据文件夹时,除了不应该使用远程代理外,使用测试仪中的文件要简单得多。另外,请注意文件名,以使经过测试的EA不会破坏实际工作的EA使用的文件。可以通过编程定义在测试器中的工作:

MQLInfoInteger(MQL5_TESTER)

测试时,请使用其他文件名。

共享对文件的访问

如前所述,如果文件已经打开,则无法第二次打开。如果一个应用程序已经处理了文件,则在关闭文件之前,另一个程序将无法访问该文件。但是,MQL5提供了共享文件的功能。打开文件时,设置其他 FILE_SHARE_READ(共享阅读) or FILE_SHARE_WRITE (shared writing) 旗。小心使用标志。当今的操作系统具有多任务处理功能。因此,不能保证写入—读取序列将正确执行。如果允许共享的读写,则可能是一个程序写入数据,而另一个程序同时读取相同(未完成)的数据。因此,我们应该采取其他措施来同步不同程序对文件的访问。这是一项复杂的任务,大大超出了本文的范围。此外,最有可能在没有同步和文件共享的情况下进行操作(当使用文件在终端之间交换数据时,将在下面显示)。

您唯一可以安全地使用共享读取(FILE_SHARE_READ)打开文件,并且这种共享是合理的情况是,使用该文件定义EA或指标操作参数时,例如配置文件。该文件是手动创建的或由其他脚本创建的,然后在初始化期间被EA或指标的多个实例读取。在初始化期间,几个EA可能会尝试几乎同时打开文件,因此您应该允许它们执行此操作。同时,保证不会同时发生读写。 

使用文件在终端之间交换数据

您可以安排将文件保存到共享文件夹的终端之间的数据交换。当然,将文件用于此类目的可能不是最佳解决方案,但在某些情况下很有用。解决方案非常简单:不使用对文件的共享访问。而是,以通常的方式打开文件。在编写过程中,没有其他人可以打开文件。写入完成后,该文件被关闭,其他程序实例无法读取它。以下是sTestFileTransmitter脚本的数据写入功能代码的代码:

bool WriteData(力量ing 力量){
   for(整型 i=0;i<30 && !IsStopped();i++){ // several attempts
      整型 h=FileOpen("data.txt",FILE_WRITE|FILE_ANSI|FILE_TXT);
      if(h!=INVALID_HANDLE){ // file opened successfully
         FileWriteString(h,str); // write the data  
         FileClose(h); // write the file
         Sleep(100); // increase the pause to let other programs 
		     // read the data
         return(真正); // return 'true' if successful
      }
      Sleep(1); // minimum pause to let other programs 
                // finish reading the file 和 catch 
                // the moment when the file is available
   }
   return(); // if writing data failed
}

多次尝试打开文件。打开文件后,进行写入,关闭和较长的暂停(Sleep(100)函数),以使其他程序打开文件。如果出现文件打开错误,则会进行短暂的暂停(Sleep(1)函数),以捕捉文件可用的时刻。

接收(阅读)功能遵循相同的原理。下面附带的sTestFileReceiver脚本具有这样的功能。获得的数据通过Comment()函数显示。在一个图表上启动发送器脚本,在另一个图表上(或在另一个终端实例中)启动接收器脚本。  

一些额外的功能

我们已经考虑了几乎所有用于处理文件的功能,除了一些很少使用的功能: 文件大小(), FileTell()FileFlush()。 FileSize()函数返回打开的文件的大小(以字节为单位):

void OnStart(){
   整型 h=FileOpen("test.txt",FILE_READ|FILE_ANSI|FILE_TXT);
   if(h==INVALID_HANDLE){
      Alert("Error opening file");
      return;
   }
   ulong size=FileSize(h);
   FileClose(h);
   Alert("File size "+IntegerToString(size)+" (bytes)");
}

可以在随附的sTestFileSize脚本中找到该代码。执行脚本时,将打开文件大小的消息窗口。 

FileTell()函数返回打开的文件的文件指针的位置。该函数很少使用,因此很难想到任何合适的示例。只需指出其存在并记住它,以备不时之需。

FileFlush()函数更有用。如文档中所述,该函数将文件输入/输出缓冲区中剩余的所有数据发送到磁盘。函数调用的效果类似于关闭和重新打开文件(尽管它更节省资源,并且文件指针保持在其初始位置)。众所周知,文件作为条目存储在磁盘上。但是,直到打开文件后,才写入缓冲区而不是磁盘。关闭文件后,将执行磁盘写操作。因此,在紧急程序终止的情况下不会保存数据。如果在每次写入文件后调用FileFlush(),则数据将保存在磁盘上,并且程序崩溃不会引起任何问题。

使用文件夹

除了使用文件之外,MQL5还具有许多用于处理文件夹的功能: FolderCreate()FolderDelete() 和 FolderClean()。 FolderCreate函数用于创建文件夹。所有功能都有两个参数。第一个对于文件夹名称是必需的。第二个是除 FILE_COMMON标志(用于处理公共数据文件夹中的文件夹)。 

FolderDelete()删除指定的文件夹。只能删除一个空文件夹。但是,清除文件夹的内容不是问题,因为为此使用FolderClean()函数。包括子文件夹和文件在内的所有内容都将被删除。 

接收文件列表

有时,您不记得确切的文件名。您可能还记得开头而不是数字结尾,例如file1.txt,file2.txt等。在这种情况下,可以使用掩码和 FileFindFirst(), FileFindNext(), FileFindClose() 职能。这些功能搜索文件和文件夹。文件夹名称可以通过在末尾加反斜杠来与文件名区分开。

让我们编写一个有用的函数来获取文件和文件夹列表。让我们在一个数组中收集文件名,而在另一个数组中收集文件夹名:

void GetFiles(力量ing folder, 力量ing & files[],力量ing & folders[],整型 common_flag=0){

   整型 files_cnt=0; // files counter
   整型 folders_cnt=0; // folders counter   
   
   力量ing name; // variable for receiving a file 要么 folder name 

    h=FileFindFirst(folder,name,common_flag); // receive a search handle 和 a name 
                                      // of the first file/folder (if present)
   if(h!=INVALID_HANDLE){ // at least a single file 要么 folder is present
      do{
         if(StringSubstr(name,StringLen(name)-1,1)=="\\"){ // folder
            if(folders_cnt>=ArraySize(folders)){ // check the array size, 
                                                 // increase it if necessary
               ArrayResize(folders,ArraySize(folders)+64);
            }
            folders[folders_cnt]=name; // send the folder name to the array
            folders_cnt++; // count the folders        
         }
         else{ // file
            if(files_cnt>=ArraySize(files)){ // check the array size, 
                                             // increase it if necessary
               ArrayResize(files,ArraySize(files)+64);
            }
            files[files_cnt]=name; // send the file name to the array
            files_cnt++; // count the files
         }
      }
      while(FileFindNext(h,name)); // receive the name of the next file 要么 folder
      FileFindClose(h); // end search
   }
   ArrayResize(files,files_cnt); // change the array size according to 
                                 // the actual number of files
   ArrayResize(folders,folders_cnt); // change the array size according to 
                                        // the actual number of folders
}

尝试使用此功能。让我们通过以下方式从脚本中调用它: 

void OnStart(){

   力量ing files[],folders[];

   GetFiles("*",files,folders);
   
   Alert("=== Start ===");
   
   for(整型 i=0;i<ArraySize(folders);i++){
      Alert("Folder: "+folders[i]);
   }      
   
   for(整型 i=0;i<ArraySize(files);i++){
      Alert("File: "+files[i]);
   }

} 

 sTestFileGetFiles脚本如下所示。注意“ *”搜索掩码:

GetFiles("*",files,folders);

该掩码允许在MQL5 / Files目录中搜索所有文件和文件夹。

为了找到所有以“ test”开头的文件和文件夹,可以使用“ test *”掩码。如果仅需要txt文件,则将需要“ * .txt”掩码,等等。创建包含多个文件的文件夹(例如,“ folder1”)。您可以使用“ folder1 \\ *”掩码来接收其包含的文件列表。 

Сode页面

在本文中,FileOpen()函数通常应用于示例代码。让我们考虑一下我们尚未描述的参数之一- 代码页。代码页是文本符号及其数值的转换表。让我们看一下ANSI编码以更清楚。编码字符表仅包含256个字符,这意味着每种语言都使用操作系统设置中定义的单独代码页。 CP_ACP常数对应于代码页,从中默认调用FileOpen()函数。某个人不太可能需要使用其他代码页,因此,对该主题进行详细介绍是没有意义的。一般知识就足够了。 

不受限制地处理文件

有时,您可能希望使用终端文件“沙箱”之外的文件(MQL5 / Files或共享文件夹之外)。这可能会大大扩展MQL5应用程序的功能,从而使您可以处理源代码文件,自动更改它们,动态生成图形界面的图像文件,生成代码等。如果您自己动手或聘请可信赖的程序员,那么你可以做到。您可以在文章中阅读更多内容 “通过WinAPI进行文件操作”。还有一种更简单的方法。 MQL5具有处理文件的所有必要方法,因此您可以将文件移至终端的“沙箱”,执行所有必要的操作然后将其移回。一个WinAPI函数(CopyFile)就足够了。

应允许使用WinAPI函数的应用程序,以便MQL5应用程序可以使用它们。在终端设置(主菜单-工具-选项-EA交易-允许DLL导入)中启用该权限。在这种情况下,将为之后启动的所有程序启用该权限。除了一般权限,您只能为要启动的程序启用权限。如果应用程序要访问WinAPI函数或其他DLL,则“设置”窗口中将显示带有“允许DLL导入”选项的“依赖关系”选项卡。

CopyFile函数有两个版本:CopyFileA()和更现代的CopyFileW()。您可以使用其中任何一个。但是,使用CopyFileA()函数时,需要首先转换字符串参数。阅读本文的“调用API函数”部分 “ MQL5编程基础:字符串” 知道细节。我建议使用最新的CopyFileW()函数。在这种情况下,字符串参数按原样指定,不需要转换。 

为了使用CopyFileW()函数,您应该首先将其导入。您可以在kernel32.dll库中找到它:

#import "kernel32.dll"
   整型 CopyFileW(力量ing,力量ing,整型);
#import

可以在随附的sTestFileWinAPICopyFileW脚本中找到该代码。

该脚本将包含其源代码的文件复制到MQL5 / Files:

void OnStart(){
   
   力量ing src=TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Scripts\\"+MQLInfoString(MQL_PROGRAM_NAME)+".mq5";
   力量ing dst=TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Files\\"+MQLInfoString(MQL_PROGRAM_NAME)+".mq5";
   
   if(CopyFileW(src,dst,0)==1){
      Alert("File copied");
   }
   else{
      Alert("Failed to copy the file");   
   }
}

如果成功,则CopyFileW()返回1,否则返回0。第三个函数参数指示如果存在目标文件,则是否可以覆盖文件:0-启用,1-禁用。启动脚本。如果成功运行,请检查MQL5 / Files文件夹。 

请注意,操作系统对文件复制施加了限制。有所谓的“用户帐户控制参数”。如果启用它们,则无法将文件复制到某些位置或从某些位置复制文件。例如,不可能将文件复制到系统驱动器的根目录。

一些有用的脚本

除了创建用于处理文件的有用功能之外,我们还创建一些有用的脚本进行更多练习。我们将开发用于将报价导出到csv文件并导出交易结果的脚本。

报价导出脚本将具有用于定义数据开始和结束日期的参数,以及用于定义是否使用日期或应导出所有数据的参数。设置必要的属性以打开脚本的属性窗口:

#property 脚本_show_inputs

随后声明外部参数:

input bool     UseDateFrom = ; // Set the start date
input datetime DateFrom=0; // Start date
input bool     UseDateTo=; // Set the end date
input datetime DateTo=0; // End date

在OnStrat()脚本函数中编写代码。根据脚本参数定义日期:

   datetime from,to;
   
   if(UseDateFrom){
      from=DateFrom;
   }
   else{
      整型 bars=Bars(Symbol(),Period());
      if(bars>0){
         datetime tm[];
         if(CopyTime(Symbol(),Period(),bars-1,1,tm)==-1){
            Alert("Error defining data start, please try again later");
            return;
         }
         else{
            from=tm[0];
         }
         
      }
      else{
         Alert("Timeframe is under construction, please try again later");
         return;
      }
   }
   
   if(UseDateTo){
      to=DateTo;
   }
   else{
      to=TimeCurrent();
   }   

如果使用日期定义变量,则使用它们的值。否则,将使用TimeCurrent()作为结束日期,而开始日期由第一个小节时间定义。 

现在我们有了日期,将引号复制到MqlRates类型数组:

   MqlRates rates[];
   
   if(CopyRates(Symbol(),Period(),from,to,rates)==-1){
      Alert("Error copying quotes, please try again later");
   }

将数组数据保存到文件中:

   力量ing FileName=Symbol()+" "+IntegerToString(PeriodSeconds()/60)+".csv";
   
   整型 h=FileOpen(FileName,FILE_WRITE|FILE_ANSI|FILE_CSV,";");
   
   if(h==INVALID_HANDLE){
      Alert("Error opening file");
      return;
   }
   
   // write data to the file in the format: Time, Open, High, Low, Close, Volume, Ticks
   
   // the first line to know the location
   FileWrite(h,"Time","Open","High","Low","Close","Volume","Ticks");  
   
   for(整型 i=0;i<ArraySize(rates);i++){
      FileWrite(h,rates[i].time,rates[i].open,rates[i].high,rates[i].low,rates[i].close,rates[i].real_volume,rates[i].tick_volume);
   }
   
   FileClose(h);

   Alert("Save complete, see the file "+FileName);   

如果成功,脚本将打开相应的消息,通知文件已成功保存。否则,将显示错误消息。现成的sQuotesExport脚本如下所示。

现在,让我们开发交易历史记录保存脚本。开始大致相同:外部变量排在第一位,尽管实现时间的定义要简单得多,因为请求历史记录时开始时间为0就足够了:

   datetime from,to;
   
   if(UseDateFrom){
      from=DateFrom;
   }
   else{
      from=0;
   }
   
   if(UseDateTo){
      to=DateTo;
   }
   else{
      to=TimeCurrent();
   }  

分配历史记录: 

   if(!HistorySelect(from,to)){
      Alert("Error allocating history");
      return;
   }

打开文件:

   力量ing FileName="history.csv";
   
   整型 h=FileOpen(FileName,FILE_WRITE|FILE_ANSI|FILE_CSV,";");
   
   if(h==INVALID_HANDLE){
      Alert("Error opening file");
      return;
   }

用字段名称写第一行:

   FileWrite(h,"Time","Deal","Order","Symbol","Type","Direction","Volume","Price","Comission","Swap","Profit","Comment");     

在遍历所有交易的同时,将买卖交易写入文件:

   for(整型 i=0;i<HistoryDealsTotal();i++){
      ulong ticket=HistoryDealGetTicket(i);
      if(ticket!=0){         
          type=HistoryDealGetInteger(ticket,DEAL_TYPE);         
         if(type==DEAL_TYPE_BUY || type==DEAL_TYPE_SELL){      
             entry=HistoryDealGetInteger(ticket,DEAL_ENTRY);      
            FileWrite(h,(datetime)HistoryDealGetInteger(ticket,DEAL_TIME),
                        ticket,
                        HistoryDealGetInteger(ticket,DEAL_ORDER),
                        HistoryDealGetString(ticket,DEAL_SYMBOL),
                        (type==DEAL_TYPE_BUY?"buy":"sell"),
                        (entry==DEAL_ENTRY_IN?"in":(entry==DEAL_ENTRY_OUT?"out":"in/out")),
                        DoubleToString(HistoryDealGetDouble(ticket,DEAL_VOLUME),2),
                        HistoryDealGetDouble(ticket,DEAL_PRICE),
                        DoubleToString(HistoryDealGetDouble(ticket,DEAL_COMMISSION),2),
                        DoubleToString(HistoryDealGetDouble(ticket,DEAL_SWAP),2),
                        DoubleToString(HistoryDealGetDouble(ticket,DEAL_PROFIT),2),
                        HistoryDealGetString(ticket,DEAL_COMMENT)                     
            );
         }
      }
      else{
         Alert("Error allocating a trade, please try again");
         FileClose(h);
         return;
      }
   }

注意:对于交易类型(买/卖)和方向(进/出),值将转换为字符串,而某些双精度型值将转换为带两个小数点的字符串。 

最后,关闭文件并显示消息: 

   FileClose(h);
   Alert("Save complete, see the file "+FileName); 

 sHistoryExport脚本如下所示。

有关该主题的更多信息

结论

在本文中,我们考虑了在MQL5中处理文件的所有功能。 尽管主题看似狭窄,但文章的篇幅却很大。 但是,一些与主题相关的问题已被粗略地检查过,并且没有足够的实际例子。无论如何,都会详细讨论最常见的任务,包括在测试仪中处理文件。此外,我们开发了许多有用的功能,所有示例都是实用且逻辑上完整的。所有代码都作为脚本附在下面。

附件

  1. sTestFileRead —从ANSI文本文件中读取一行,并将其显示在消息框中。
  2. sTestFileReadToAlert —从ANSI文本文件中读取所有行,并将其显示在消息框中。
  3. sTestFileCreate —创建ANSI文本文件。
  4. sTestFileAddToFile —在ANSI文本文件中添加一行。
  5. sTestFileChangeLine2-1 —尝试更改ANSI文本文件中的单行的无效尝试。
  6. sTestFileChangeLine2-2-另一个无效的尝试更改ANSI文本文件中的一行。
  7. sTestFileChangeLine2-3 —通过重写整个文件来替换ANSI文本文件中的一行。
  8. sTestFileReadFileToArray —有用的函数,用于将ANSI文本文件读取到数组。
  9. sTestFileCreateCSV —创建ANSI CSV文件。
  10. sTestFileReadToAlertCSV —按字段将ANSI CSV文件读取到消息框中。
  11. sTestFileReadToAlertCSV2 —通过行分隔字段将ANSI CSV文件读取到消息框中。 
  12. sTestFileReadFileToArrayCSV —将ANSI CSV文件读取到结构数组。
  13. sTestFileWriteArrayToFileCSV —将数组作为一行写入CSV ANSI文件。
  14. sTestFileReadToAlertUTF —读取UNICODE文本文件并将其显示在消息框中。
  15. sTestFileCreateUTF —创建UNICODE文本文件。
  16. sTestFileCreateBin —创建二进制文件并向其中写入三个double变量。
  17. sTestFileReadBin —从二进制文件读取三个双精度变量。
  18. sTestFileChangeBin —重写二进制文件中的第二个double变量。
  19. sTestFileReadBin2 —从二进制文件读取第三个double变量。 
  20. sTestFileWriteStructBin —将结构写入二进制文件。
  21. sTestFileReadStructBin —从二进制文件读取结构。
  22. sTestFileReadStructBin2 —从具有结构的二进制文件中读取单个变量。
  23. sTestFileCheckUnicode —检查文件类型(ANSI或UNCODE)。
  24. sTestFileWriteArray —将数组写入二进制文件。
  25. sTestFileReadArray —从二进制文件读取数组。
  26. sTestFileWriteArray2 —将两个数组写入二进制文件。
  27. sTestFileReadArray2 —从二进制文件读取两个数组。
  28. sTestFileWriteStringArray —将字符串数组写入二进制文件。
  29. sTestFileReadStringArray —从二进制文件读取字符串数组。
  30. sTestFileCopy-将文件从MQL5 / Files复制到共享文件夹。
  31. sTestFileCopy2 —将文件复制到共享文件夹。
  32. sTestFileCopy3-将文件从共享文件夹复制到MQL5 / Files。 
  33. sTestFileTransmitter —用于通过共享中的文件传输数据的脚本 folder.
  34. sTestFileReceiver —脚本,用于通过共享数据文件夹中的文件接收数据。
  35. sTestFileSize —定义文件大小。
  36. sTestFileGetFiles —通过掩码接收文件列表。
  37. sTestFileWinAPICopyFileW —使用WinAPI CopyFileW()函数的示例。
  38. sQuotesExport —用于导出报价的脚本。
  39. sHistoryExport —用于保存交易历史的脚本。

由MetaQuotes Software Corp.从俄语翻译而来。
来源文章: //www.tbxfkj.com/ru/articles/2720

附加的文件 |
files.zip (27.47 KB)
最后评论| 去讨论 (2)
可怜的人
可怜的人 | 2017年12月4日20:51

这篇文章很棒,非常感谢您提供的信息。

罗西米尔·马捷夫(Rosimir Mateev)
罗西米尔·马捷夫(Rosimir Mateev) | 2018年7月25日在18:56

Акакможноуменшитьразмер файла? ПримерноимеюкакойтоBINфайлвкоторойподдерживаюкакиетоданни,потомделаюдефрагментфайлаинаконецхочуумешнитегодлина,потомучтосзадиужеестьлишноепространство。 Какетогосделать? Нужнакакаятафункциятипа FileResize(int newSize)。

图形界面X:便捷库的更新(内部版本3) 图形界面X:便捷库的更新(内部版本3)

本文介绍了Easy 和 Fast库的下一版本(版本3)。修复了某些缺陷并添加了新功能。本文中将进一步介绍更多详细信息。

神经网络:自我优化的EA交易 神经网络:自我优化的EA交易

是否有可能开发出能够根据代码命令定期优化仓位开仓和平仓条件的EA交易?如果我们以模块的形式实现神经网络(多层感知器)以分析历史并提供策略,会发生什么?我们可以使EA每月(每周,每天或每小时)优化神经网络,然后继续其工作。因此,我们可以开发一种自我优化的EA。

图形界面X:标准图表控件(内部版本4) 图形界面X:标准图表控件(内部版本4)

这次我们将考虑标准图表控件。它将允许创建具有同步水平滚动功能的子图表数组。此外,我们将继续优化库代码以减少CPU负载。

MQL5编程基础:MetaTrader 5终端的全局变量 MQL5编程基础:MetaTrader 5终端的全局变量

终端的全局变量为开发复杂而可靠的EA交易提供了必不可少的工具。如果您掌握了全局变量,那么您将无法想象没有它们在MQL5上开发EA。