English Русский Español Deutsch 日本語 Português
通过 WINAPI 进行文件操作

通过 WINAPI 进行文件操作

MetaTrader 4示例 | 17 三月 2016, 11:26
2 937 1
MetaQuotes
MetaQuotes

简介

MQL4 设计的理念是,即便是编写错误的程序也无法误删硬盘中的数据。用于 文件读取和编写操作 的函数仅可用于以下目录(报价):

  • /HISTORY/<current broker> - 尤其对于 FileOpenHistory 函数;
  • /EXPERTS/FILES - 普通情况;
  • /TESTER/FILES - 尤其针对测试。
禁止处理其他目录的文件。

如果你仍需在目录以外工作(出于安全原因),你可以调用 Windows OS 的函数。为此,出现在 kernel32.dll 库中的 API 函数已广泛使用。

Kernel32.dll 的文件函数

是基于 无限制文件操作下 CodeBase 内发现的脚本。这是如何将函数导入 MQL4 程序的很好例子。

// constants for function _lopen
#define OF_READ               0
#define OF_WRITE              1
#define OF_READWRITE          2
#define OF_SHARE_COMPAT       3
#define OF_SHARE_DENY_NONE    4
#define OF_SHARE_DENY_READ    5
#define OF_SHARE_DENY_WRITE   6
#define OF_SHARE_EXCLUSIVE    7
 
 
#import "kernel32.dll"
   int _lopen  (string path, int of);
   int _lcreat (string path, int attrib);
   int _llseek (int handle, int offset, int origin);
   int _lread  (int handle, string buffer, int bytes);
   int _lwrite (int handle, string buffer, int bytes);
   int _lclose (int handle);
#import

这些函数在 msdn 内显示为过时,但仍可以使用,请参见 Obsolete Windows Programming Elements(过时的 Windows 编程元素)。我将在此给出从线程中直接取出的函数和参数的描述,这些描述均由脚本作者 mandor给出:

// _lopen  : It opens the specified file. It returns: file descriptor.
// _lcreat : It creates the specified file. It returns: file descriptor.
// _llseek : It places the pointer in the open file. It returns: 
// the new shift of the pointer.
// _lread  : It reads the given number of bytes from the open file. 
// It returns: the number of the read bytes; 0 - if it is the end of the file.
// _lwrite : It writes the data from buffer into the specified file. It returns: 
// the number of written bytes.
// _lclose : It closes the specified file. It returns: 0.
// In case of unsuccessfully completion, all functions return the value of 
// HFILE_ERROR=-1.
 
// path   : String that defines the path and the filename.
// of     : The way of opening.
// attrib : 0 - reading or writing; 1 - only reading; 2 - invisible, or 
// 3 - system file.
// handle : File descriptor.
// offset : The number of bytes, by which the pointer shifts.
// origin : It indicates the initial point and the shifting direction: 0 - 
// forward from the beginning; 1 - from the current position; 2 - backward from the end of the file.
// buffer : Receiving/writing buffer.
// bytes  : The number of bytes to read.
 
// Methods of opening (parameter 'of'):
// int OF_READ            =0; // Open file for reading only
// int OF_WRITE           =1; // Open file for writing only
// int OF_READWRITE       =2; // Open file in the read/write mode
// int OF_SHARE_COMPAT    =3; // Open file in the mode of common 
// shared access. In this mode, any process can open this given 
// file any amount of times. At the attempt to open this file in any other
// mode, the function returns HFILE_ERROR.
// int OF_SHARE_DENY_NONE =4; // Open file in the mode of common access 
// without disabling the reading/writing by another process. At the attempt to open 
// this file in the mode of OF_SHARE_COMPAT, the function returns HFILE_ERROR.
// int OF_SHARE_DENY_READ =5; // Open file in the mode of common access with 
// disabling the reading by another process. At the attempt to open this file 
// with the flags of OF_SHARE_COMPAT and/or OF_READ, or OF_READWRITE, the function 
// returns HFILE_ERROR.
// int OF_SHARE_DENY_WRITE=6; // The same, but with disabling the writing.
// int OF_SHARE_EXCLUSIVE =7; // Disable for this current and for all other processes 
// to access to this file in the modes of reading/writing. The file in this mode can be 
// opened only once (with the current process). All other attempts 
// to open the file will fail.

函数 “Reading from File”

让我们讨论下用于文件读取的函数。该函数仅有的参数是包含文件名的字符串变量。导入的函数 _lopen(path,0) 返回指向一个开放文件的指针,其任务与 MQL4 中的函数 FileOpen() 很相似。

//+------------------------------------------------------------------+
//|   read the file and return a string with its contents            |
//+------------------------------------------------------------------+
string ReadFile (string path) 
  {
    int handle=_lopen (path,OF_READ);           
    if(handle<0) 
      {
        Print("Error opening file ",path); 
        return ("");
      }
    int result=_llseek (handle,0,0);      
    if(result<0) 
      {
        Print("Error placing the pointer" ); 
        return ("");
      }
    string buffer="";
    string char1="x";
    int count=0;
    result=_lread (handle,char1,1);
    while(result>0) 
      {
        buffer=buffer+char1;
        char1="x";
        count++;
        result=_lread (handle,char1,1);
     }
    result=_lclose (handle);              
    if(result<0)  
      Print("Error closing file ",path);
    return (buffer);
  }

函数 _lseek() 在 MQL 4 内也有相似函数。它是 FileSeek()。函数 _lclose 用于关闭文件,如函数 FileClose()。仅有的新函数为_lread(handle, buffer, bytes),它将给定文件(该文件指针最初须由函数 _lopen() 接收)给定字节数读取到变量“buffer”。你应使用必要长度的字符串常量作为“buffer”变量。在此示例中,我们可以看到:

    string char1="x";
    result=_lread (handle,char1,1);

- 字符串常量 ‘char’ 给定长度为 1,即该常量仅允许一个字节读取到其中。同时,此常量的值无关紧要,可为 “x”和“Z” ,甚至是" " (空白字符)。你无法向常量中读取超出其初始定义的字节数。在这种情况下,读取 2 或者更多字节的尝试将失败。此外,函数 _lread() 的结果是真正读取的字节数。如果文件大小是 20 字节,且你尝试在 30-字节长的变量内读入超出 20 字节的内容,则函数将返回 20。如果我们连续应用该函数,则我们将沿一个接一个读取文件块的文件方向移动。例如,文件大小为 22 字节。我们以 10 字节块开始读取。然后,经过两次调用函数 __lread(handle, buff, 10),文件末端的字节将维持未读状态。

第三次调用时,__lread(handle, buff, 10)将返回 2,即最后的 2 字节将读取。第四次调用时,函数将返回零值 - 没有字节读取,指针位于文件末端。这个事实构成了从循环中读取文件字符的过程:

    while(result>0) 
      {
        buffer=buffer+char1;
        char1="x";
        count++;
        result=_lread (handle,char1,1);
     }

只要结果(读取字节数)大于零,函数 _lread(handle, char1, 1)便在循环中调用。如你所见,这些函数并不复杂。读取字符的值保存在名为 char1 的变量内。该字符在下次迭代时从字符串变量“buffer”中写入。完成操作时,用户定义的函数 ReadFile() 返回该在变量读取的文件内容。如你所见,这不会有任何困难。


函数 "Writing to File"

写入在某种意义上来说比读取更简单。你应打开一个文件然后使用函数(int handle, string buffer, int bytes)向其中写入一条字节数组。在此,handle 是由函数 _lopen() 获得的文件指针,参数‘buffer’是字符串变量,参数‘bytes’显示应写入的字节数量。写入时,应使用函数 _lclose() 关闭文件。让我们讨论作者的函数 WriteFile():

//+------------------------------------------------------------------+
//|  write the buffer contents to the given path                     |
//+------------------------------------------------------------------+
void WriteFile (string path, string buffer) 
  {
    int count=StringLen (buffer); 
    int result;
    int handle=_lopen (path,OF_WRITE);
    if(handle<0) 
      {
        handle=_lcreat (path,0);
        if(handle<0) 
          {
            Print ("Error creating file ",path);
            return;
          }
        result=_lclose (handle);
     }
    handle=_lopen (path,OF_WRITE);               
    if(handle<0) 
      {
        Print("Error opening file ",path); 
        return;
      }
    result=_llseek (handle,0,0);          
    if(result<0) 
      {
        Print("Error placing pointer"); 
        return;
      }
    result=_lwrite (handle,buffer,count); 
    if(result<0)  
        Print("Error writing to file ",path," ",count," bytes");
    result=_lclose (handle);              
    if(result<0)  
        Print("Error closing file ",path);
  }

不过,我们应进行错误检查。首先,我们应尝试打开文件以写入:

    int handle=_lopen (path,OF_WRITE);

(使用参数 OF_WRITE 调用函数 _lopen())。

如果尝试失败(handle < 0),则尝试创建文件,指定名为:

        handle=_lcreat (path,0);

如果函数同样返回负指针,则函数 WriteFile() 应删减。此函数内剩余代码很清楚,无需进一步解释。最简单的 start() 函数允许你检查脚本 File_Read_Write.mq4 是如何运作的。

//+------------------------------------------------------------------+
//| script program start function                                    |
//+------------------------------------------------------------------+
int start()
  {
//----
    string buffer=ReadFile("C:\\Text.txt");
    int count=StringLen(buffer);
    Print("Bytes counted:",count);
    WriteFile("C:\\Text2.txt",buffer);   
//----
   return(0);
  }
//+------------------------------------------------------------------+

请注意,反斜线 (”\”) 写入了两次,尽管其必须写入一次。事实是,部分特殊字符,如换行字符(”\n”) 或分隔符(”\t”)必须使用反斜线写入。如果你忘记这个事实,则你可能在程序执行期间对测试变量中路径的指定遇到问题。

请在字符串常量内写入两个连续的后斜线,而不仅是一个。在此情况下,编译器将明确无误地接受。


一切都运作顺畅,但美中不足的是:脚本函数 ReadFile() 运行大文件时非常缓慢。

读取文件缓慢的原因在于我们是以一个字节接一个字节(字符)的方式读取信息的。你可以在以上图表中看到,大小为 280 324 字节的文件读取需要 103 秒。用时以读取 1 个字符 280 324 次所占时间计算。你可以自行检查本文附带的脚本 File Read Write.mq4 的运行时间。我们如何加速文件读取速度呢?答案就在我们面前 - 函数必须一次读取 50 个字符,而不是一次一个字符。_lread() 函数调用次数将降低 50 倍。因此,读取时间也必须降低 50 倍。让我们开始做吧。

按块读取文件的新函数为每个 50 字符。

将命名新版本的代码更改为 xFiles,mq4。对其进行编译并启动运行。请记住,从 DLLs 导入的函数必须在设定中启用(Ctrl+O)。

因此,修改后的脚本 xFiles.mq4 运行时间为 2047 毫秒,约为 2秒。将 103 秒(初始脚本的运行时间)除以 2 秒,可获得 103 / 2 = 51.5 51.5 倍。因此,程序运行时间果真如预期的改变了近 50 倍。如何修改代码以获得此效果?

修改其实很细微:


string ReadFile (string path) 
  {
    int handle=_lopen (path,OF_READ);
    int read_size = 50;           
    string char50="x                                                 ";
 
    if(handle<0) 
      {
        Print("Error opening file ",path); 
        return ("");
      }
    int result=_llseek (handle,0,0);      
    if(result<0) 
      {
        Print("Error placing the pointer" ); 
        return ("");
      }
    string buffer="";
    int count=0;
    int last;
    
    result=_lread (handle,char50,read_size);
    int readen;
    while(result>0 && result == read_size) 
      {
        buffer=buffer + char50;
        count++;
        result=_lread (handle,char50,read_size);
        last = result;
     }
    Print("The last read block has the size of, in bytes:", last);
    char50 = StringSubstr(char50,0,last);
    buffer = buffer + char50;    
    result=_lclose (handle);              
    if(result<0)  
      Print("Error closing file ",path);
    return (buffer);
  }

请注意,字符串变量 ‘char50’ 现已由 50 字符的常量所初始化(字符 “x” 和 49 个空白字符)。

现在,我们可以通过一次读取 50 个字节(字符)的方式将文件读取到此变量中:

result=_lread (handle,char50,read_size);

在此:read_size = 50。当然,文件大小不太可能总是以 50 字节的倍数进行读取,这说明此函数运行值有时可不同于 50。这便是停止循环的信号。最后读取到变量的字符块将缩减至真正读取的字节数。

你可以通过函数 lread() 改变读取至 N 次的缓冲区大小,但切勿忘记做两处修改:

  1. 设定 read_size 值为 ‘N’;
  2. 初始化常量长度 为 N 的字符串变量 ‘char50’ (N<256)。

我们就这样加速了读取操作。余下的最后一项任务是在尝试写入文件时处理不存在路径的错误。在函数 WriteFile() 内,尝试创建文件,但情况未处理,没有文件夹包含前往文件名的路径。所以我们需要另一个函数 -

创建文件夹函数

创建文件夹函数也可见于 kernel32.dll - CreateDirectory Function。仅需要注意的是,此函数仅尝试创建最下层的文件夹,不会创建所有路径上的中间文件夹(如果这些文件夹未出现的话)。例如,如果我们尝试使用此函数来创建文件夹 “C:\folder_A\folder_B”,该尝试仅会在 “C:/folder_A” 路径在函数调用前就存在的前提下成功。否则,folder_B 将无法创建。让我们将新函数添加至导入部分:

#import "kernel32.dll"
   int _lopen  (string path, int of);
   int _lcreat (string path, int attrib);
   int _llseek (int handle, int offset, int origin);
   int _lread  (int handle, string buffer, int bytes);
   int _lwrite (int handle, string buffer, int bytes);
   int _lclose (int handle);
   int CreateDirectoryA(string path, int atrr[]);
#import

第一个参数包含创建新文件夹的路径,而第二个参数 atrr[] 则用于指定待创建文件夹的权限,且该权限必须为 _SECURITY_ATTRIBUTES 类型。我们将不会为第二个参数提供任何信息,但会传递名为 ’int’ 的空数组。在此情况下,待创建的文件夹将继承父文件夹中的所有权限。然而,在尝试应用此函数前,我们必须执行这些操作 -

分解路径

确实,我们必须创建 4 级文件夹,例如:

“C:\folder_A\folder_B\folder_C\folder_D"

我们在此称 folder_D 为 4 级文件夹,因为在其之上已有 3 级文件夹。‘C:’ 盘包含 “folder_A”, 文件夹 “C:\folder_A\” 包含 “folder_B”, 文件夹 “C:\folder_A\folder_B\” 包含”folder_C; 以此类推。这说明我们必须将文件整个路径分解至子文件夹数组内。让我们将必要的函数命名为 ParsePath():

//+------------------------------------------------------------------+
//| break apart the path  into an array of subdfolders               |
//+------------------------------------------------------------------+
bool ParsePath(string & folder[], string path)
   {
   bool res = false;
   int k = StringLen(path);
   if (k==0) return(res);
   k--;
 
   Print("Parse path=>", path);
   int folderNumber = 0;
//----
   int i = 0;
   while ( k >= 0 )
      {
      int char = StringGetChar(path, k);
      if ( char == 92) //  back slash "\"
         {
         if (StringGetChar(path, k-1)!= 92)
            {
            folderNumber++;
            ArrayResize(folder,folderNumber);
            folder[folderNumber-1] = StringSubstr(path,0,k);
            Print(folderNumber,":",folder[folderNumber-1]);
            }
         else break;         
         }
      k--;   
      }
   if (folderNumber>0) res = true;   
//----
   return(res);   
   }   
//+------------------------------------------------------------------+

文件夹之间的分隔符为字符 ‘\’,该字符在 ANSI 代码中值为 92。函数获得 ‘path’ 作为其参数,并填写数组命名的文件夹[],传递给已找到路径名的函数,始于最低级别,收于最高级别。在此示例中,数组将包含以下值:

folder[0] =“C:\folder_A\folder_B\folder_C\folder_D"
folder[1] = "C:\folder_A\folder_B\folder_C";
folder[2] = "C:\folder_A\folder_B";
folder[3] = "C:\folder_A";
folder[4] = "C:";

如果我们想写入名为“C:\folder_A\folder_B\folder_C\folder_D\test.txt”的文件,则可以将指定路径分解进文件名 test.txt 和包含此文件的子文件夹 ”C:\folder_A\folder_B\folder_C\folder_D” 结构内。如果程序无法在此路径上创建文件,则首先应尝试创建最低级别文件夹,C:\folder_A\folder_B\folder_C\folder_D”。

如果创建此文件夹的尝试同样失败,则父文件夹 "C:\folder_A\folder_B\folder_C" 很可能缺失。因此我们将创建级别越来越高的文件夹,直到我们获得函数 CreateDirectoryA() 成功完成的消息。这就是为何我们需要能够填写带有升序排列的文件夹名称的字符串数组 ‘folder[]’。第一个零索引包含最低级别的文件夹,根目录则位于最新的数组索引内。

现在我们可以组合函数,以创建在给定路径中所有必要的中间文件夹:

//+------------------------------------------------------------------+
//|  It creates all necessary folders and subfolders                 |
//+------------------------------------------------------------------+
bool CreateFullPath(string path)
   {
   bool res = false;
   if (StringLen(path)==0) return(false);
   Print("Create path=>",path);
//----
   string folders[];
   if (!ParsePath(folders, path)) return(false);
   Print("Total subfolders:", ArraySize(folders));
   
   int empty[];
   int i = 0;
   while (CreateDirectoryA(folders[i],empty)==0) i++;
   Print("Create folder:",folders[i]);
   i--;
   while (i>=0) 
      {
      CreateDirectoryA(folders[i],empty);
      Print("Created folder:",folders[i]);
      i--;
      }
   if (i<0) res = true;   
//----
   return(res);
   }

现在,仅需要在函数 WriteFile() 中进行细微修改,就有可能创建新的文件夹了。

//+------------------------------------------------------------------+
//|  write the buffer contents to the given path                     |
//+------------------------------------------------------------------+
void WriteFile (string path, string buffer) 
  {
    int count=StringLen (buffer); 
    int result;
    int handle=_lopen (path,OF_WRITE);
    if(handle<0) 
      {
        handle=_lcreat (path,0);
        if(handle<0) 
          {
            Print ("Error creating file ",path);
            if (!CreateFullPath(path))
               {
               Print("Failed creating folder:",path);
               return;
               }
            else handle=_lcreat (path,0);   
          }
        result=_lclose (handle);
        handle = -1;
     }
    if (handle < 0) handle=_lopen (path,OF_WRITE);               
    if(handle<0) 
      {
        Print("Error opening file ",path); 
        return;
      }
    result=_llseek (handle,0,0);          
    if(result<0) 
      {
        Print("Error placing the pointer"); 
        return;
      }
    result=_lwrite (handle,buffer,count); 
    if(result<0)  
        Print("Error writing to file ",path," ",count," bytes");
    result=_lclose (handle);              
    if(result<0)  
        Print("Error closing file ",path);
    return;        
  }

修改后的函数运作原理如下图所示。

请注意,新创建的文件关闭后,我们将文件描述符变量 ‘handle’ 设为负值。

        result=_lclose (handle);
        handle = -1;

此举是为了检查下一行的 ‘handle’ 值并在首次打开失败后打开仅供读取的文件。

    if (handle < 0) handle=_lopen (path,OF_WRITE);

此举允许我们避开多个文件错误打开且没有关闭的情况。在这些情况下,操作系统将告知打开文件超出最大允许数量,且不允许我们打开新的文件。

让我们修改函数 start() 以检查新的特性:

//+------------------------------------------------------------------+
//| script program start function                                    |
//+------------------------------------------------------------------+
int start()
  {
//----
    int start = GetTickCount();
    string buffer=ReadFile("C:\\Text.txt");
 
    int middle = GetTickCount();
    int count=StringLen(buffer);
 
    Print("Bytes read:",count);
 
    WriteFile("C:\\folder_A\\folder_B\\folder_C\\folder_D\\Text2.txt",buffer);   
    int finish = GetTickCount();
    Print("File size is ",count," bytes. Reading:",(middle-start)," ms. Writing:",(finish-middle)," ms.");
//----
   return(0);
  }
//+------------------------------------------------------------------+

运行执行脚本 script xFiles.mq4。


总结

使用 WinAPI 函数并非难事,但你应记住离开“沙盒”的反面:

启动带有扩展名为 ex4(没有 MQL4 源代码情况下) 的未知可执行程序前,需要从外部 DLL 导入函数,在此之前,考虑清楚可能后果。

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/1540

附加的文件 |
xFiles.mq4 (8.59 KB)
最近评论 | 前往讨论 (1)
秋风踏浪
秋风踏浪 | 4 11月 2018 在 14:05
MetaQuotes Software Corp.:

新文章 通过 WINAPI 进行文件操作已发布:

作者:MetaQuotes Software Corp.

下载文章中一模一样的代码,在MT4平台中做了一个程序,又在硬盘上做了一个文本文件 C:\Text.txt,并在文本文件里保存了字符串。运行程序,读不出文本文件里的字符串,也不能创建 C:\Text2.txt 文本文件。不知道是什么原因。

以前版本的MT4,自己编写.dll文件,操作目录外的文本文件都没有问题。现在不管怎么弄,都无法成功操作目录外的文本文件。

捕捉趋势 捕捉趋势
本文描述了对获利交易的成交量增加的算法。本文对使用 MQL4 语言实现该算法进行了描述。
MetaEditor:模板作为支点 MetaEditor:模板作为支点
可一次性为 EA 编写完成所有准备工作并且这些准备工作可供以后持续使用,这对我们很多读者而言可能还有点陌生。
基于大众交易系统和交易机器人优化点金术的 Expert Advisor 基于大众交易系统和交易机器人优化点金术的 Expert Advisor
本文介绍最简单交易系统的实现算法。本文对交易新手和 EA 编写者比较有帮助。
基于大众交易系统和交易机器人优化点金术的 Expert Advisor(续) 基于大众交易系统和交易机器人优化点金术的 Expert Advisor(续)
在本文中,作者将给出符合 2008 年自动交易锦标赛规则所载要求的 Expert Advisor 例子