English Русский 中文 Español Deutsch Português
WinAPI によるファイル処理

WinAPI によるファイル処理

MetaTrader 4 | 21 4月 2016, 16:53
3 256 0
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 内で廃止として宣言されていますが、まだ使用されています。廃止のウィンドウズプログラミング要素を参照ください。ここではそのスレッドから直接取られた関数およびパラメータの記述を提供します。それらはスクリプトの著者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.


『ファイルから読み出す』関数

ファイルからの読み出しを行う関数を考察します。その唯一のパラメータはファイル名を持つ文字列変数です。インポートされた関数は開いているファイルのポインターを返し、そのタスクについては 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() にも MQL4 に類似体があります。それは FileSeek() です。関数 _lclose は、関数 FileClose() のようにファイルを閉じるのに使用されます。唯一の新しい関数は _lread(handle, buffer, bytes) で、これは所定のファイル(関数 _lopen() で事前に受け取られるポインター)から変数 'buffer' に所定のバイト数を読み出します。'buffer ' 変数として必要な長さの文字列定数を使用します。この例では以下となります。

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

-長さが1の文字列定数 'char' が与えられます。すなわち、それによりそこに1バイトだけ読むことができるのです。同時に、この定数の値は問題ではありません。 "x"、"Z" のどちらでもありえ、" "(空白文字)でもかまいません。この定数に最初に定義されている以上のバイト数は読むことができません。この場合、2バイト以上を読もうとしてもうまくいきません。また、関数 _lread() の結果は実際に読むバイト数です。ファイルの大きさが20倍とで、30バイトの長さの変数を読もうとしたら、関数は20を返します。この関数を連続して使えば、一つずつファイルのブロックを読みながらファイルを移動することになります。たとえば、あるファイルの長さが22バイトだとします。10バイトのブロックごとに読み始めます。そして、関数 __lread(handle, buff, 10) を二度呼んだあと、ファイルの終わりの2バイトかは未読のままとなります。


3度目の呼出しで、__lread(handle, buff, 10) は2を返します。すなわち、最後の2バイトが読まれるのです。4度目の呼出しで関数は値ゼロを返します。読まれるバイトはなく、ポインターはファイルの終わりにあります。それは、サイクルでファイルから文字を読む手順の基礎となっている、ということです。

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

結果(読みだされるバイト数)がゼロより大きい限り、関数 _lread(handle, char1, 1) はサイクルで呼び出されます。ごらんのように、これら関数にはなんらむつかしいことはありません。読まれる文字の値は char1 という名前の変数に保存されます。この文字は、次の反復時に文字列変数 'buffer' から書かれます。処理完了時、ユーザー定義関数 ReadFile() がこの変数内で読まれたファイルの内容を返します。見てのとおり、むつかしいことはありません。


『ファイルに書き込む』関数

書き込みはある意味読み出しよりも簡単です。ファイルを開き、そこに関数 _lwrite (int handle, string buffer, int bytes)によってバイト配列を書きこむのです。ここで、ハンドルは関数 _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);

(関数 _lopen() はパラメータ OF_WRITEで呼ばれます)。

その試みが失敗すると(ハンドル < 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() は大きなファイルを処理するのにひじょうにスピードが遅いのです。


ファイルを読む速度がそんなに遅い理由は、情報を1バイト(文字)ずつ読むためです。ファイルサイズが 280,324 バイトのものを読むのに103秒かかるのが上の例でわかります。この時間は1文字を 280,324 回読むのにかかるものです。本稿添付のスクリプト File Read Write.mq4 に対する作業時間を個人的にに確認することができます。ファイルからの読み出しをスピードアップするには?答えは知れています。関数は一度に1文字ずつではなくたとえば50文字などを読む必要がある、ということです。そうすると、_lread() 関数の呼出し回数は50倍減ります。よって読み出し時間も50倍減ることとなります。それを確認します。

各50倍とのブロックでファイルを読む新しい関数

コードの名前を新しいバージョン xFiles,mq4 として変更します。コンパイルし、実行のために起動します。DLLs からのインポート関数は設定(Ctrl+O)で有効にする必要があることをここで思い出します。



修正したスクリプトxFiles.mq4 の実行時間は 2,047 ミリ秒となりました。これはおよそ2秒です。103秒(最初のスクリプトの実行時間)を2秒で割ると 103 / 2 = 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 サイズに変更可能ですが、2つの変更を忘れないでください。

  1. read_size の値を 'N' に設定します。
  2. N(N<256)の定数長の文字列変数 'char50' を初期化します。

これで読み出し処理をスピードアップしました。残っている最後のタスクはファイルの書き込みをしようとするとき、存在しないパスのエラーを処理することです。関数 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

第1のパラメータは新規フォルダ作成のパスを持っています。同時に第2のパラメータ atrr[] は作成されるフォルダに対する許可を指定する役割をし、_SECURITY_ATTRIBUTES タイプでなければなりません。第2パラメータについては情報を提供しませんが、'int' という名前の空の配列だけ渡します。この場合、作成されるフォルダは親フォルダからすべての許可を受け継ぎます。ただ、この関数を適用する前に、次のような操作を行うことが必要です。

パスを分ける

第4レベルのフォルダを作成します。たとえば以下のようなフォルダです。

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

ここで、 folder_D を第4レベルフォルダと呼びます。というのも、その上位に3つのフォルダレベルがあるためです。Disk '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;

これを行うのは、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);
  }
//+------------------------------------------------------------------+

そして実行のためスクリプト xFiles.mq4 を起動します。



おわりに

関数を使用することはそれほどむつかしくありませんが、『サンドボックス』を残す逆の面を覚えておく必要があります。

外部 DLL から関数をインポートするよう要求する、ex4 の拡張子を持つ未知の実行可能アプリケーション(MQL4 ソースコードのない)を起動する前に、可能性ある結果について考えてください。



MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/1540

添付されたファイル |
xFiles.mq4 (8.59 KB)
一般的なトレーディングシステムを基にしたExpert Advisor と売買ロボット最適化の錬金術(パート7) 一般的なトレーディングシステムを基にしたExpert Advisor と売買ロボット最適化の錬金術(パート7)
本稿では、「自動売買チャンピオンシップ 2008 のルール」で述べられている要件を満たす Expert Advisor 例を提供します。
再起動なしでの MQL4 プログラムの外部パラメータ変更 再起動なしでの MQL4 プログラムの外部パラメータ変更
本稿では、再起動せずオンザフライで MQL4 プログラムの外部パラメータを変更する方法について説明します。
初心者の記録: ZigZag 初心者の記録: ZigZag
確かに、初めて不可解な多角形を見たとき、極値に近くトレードをするという異常な思考が見習いトレーダー全員に訪れます。実際それはとても単純です。ここに最大値があります。そしてそこに最小値があります。履歴には美しい絵があります。そして、実際には何でしょう?線が描かれます。それは頂点のように見えます。売るタイミングです。そして次に下がっていきます。絶対にノーです!価格は裏切り上向きに変動しています。ホー!ささいなことです。インディケータではないのです。そして投げ出すのです!