Файлові операції через WinAPI

  1. Вступ Мова MQL4 спроектований таким чином, щоб навіть неправильно написані програми не могли випадково...
  2. Функція читання з файлу
  3. Функція запису в файл
  4. Нова функція читання файлу блоками по 50 байт.
  5. Функція створення папок
  6. Розбір шляху на складові
  7. висновок

Вступ

Мова MQL4 спроектований таким чином, щоб навіть неправильно написані програми не могли випадково видалити дані з жорсткого диска комп'ютера. функції операцій читання і запису у файли можуть працювати тільки в наступних директоріях (цитую):

Якщо все ж вам необхідно працювати поза заданих (з міркувань безпеки) папок, то ви можете звернутися до функцій операційної системи Windows. Для цього широко використовуються функції API, представлені в бібліотеці kernel32.dll.


Файлові функції бібліотеки kernel32.dll

За основу був узятий скрипт, викладений в гілці Файлові операції в MQL4 без обмежень . Це хороший приклад імпорту функцій в програму на MQL4.

#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 . Опис функцій і параметрів я приведу прямо з тієї гілки, як це зробив автор скрипта - mandor:


Функція читання з файлу

Розглянемо запропоновану фукнции для читання з файлу. Есдінственним її параметром є змінна типу string, в якій міститься ім'я файлу. Імпортована функція _lopen (path, 0) повертає покажчик на відкритий файл, і за призначенням дуже схожа на функцію FileOpen () з MQL4.

string ReadFile (string path) {int handle = _lopen (path, OF_READ); if (handle <0) {Print ( "Помилка відкриття файлу", path); return ( ""); } Int result = _llseek (handle, 0, 0); if (result <0) {Print ( "Помилка установки покажчика"); 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 ( "Помилка закриття файлу", path); return (buffer); }

Функція _lseek () також має аналог в MQL4 - це FileSeek () . Для закриття файлу служить функція _lclose, яка використовується також, як і FileClose () . Єдиною новою функцією є _lread (handle, buffer, bytes), яка читає із зазначеного файлу (покажчик на який повинен бути попередньо отриманий функцією _lopen ()) в змінну buffer вказану кількість байт. Як зміною buffer необхідно використовувати строкову константу необхідної довжини. В даному приклад ми бачимо:

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

-

вказана строкова константа char, яка має одиничну довжину, тобто дозволяє вважати в неї тільки один байт. Причому значення цієї константи не впливає, це може бути не "х", а "Z" або навіть "" (знак пробілу). Ви не зможете вважати в неї більшу кількість байт, ніж було розподілено для цієї константи спочатку. В даному випадку, спроби вважати 2 або більше байт успіхом не увінчаються. Крім того, результатом функції _lread () є кількість реально лічених байтів. Якщо файл має довжину в 20 байт, а ви спробуєте вважати в змінну довжиною 30 байт більше 20 байт, що функція поверне значення 20. Якщо застосовувати цю функцію послідовно, то ми будемо рухатися по файлу, читаючи один блок байтів за іншим. Наприклад, файл має розмір 22 байти, ми починаємо його зчитувати блоками по 10 байт, тоді після двох викликів функції __lread (handle, buff, 10) залишаться непрочитаними два байта в кінці файлу.

Наприклад, файл має розмір 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); }

До тих пір, поки result (кількість прочитаних байтів) більше нуля, відбувається циклічний виклик функції _lread (handle, char1, 1). Як бачите, нічого складного в цих функціях немає. Значення ліченого символу зберігається в змінної char1 і на наступній ітерації цей символ дописується з строкової змінної buffer. Після закінчення роботи для користувача функція ReadFile () повертає вміст прочитаного файлу в цій змінній. Як бачите, нічого складного в цьому немає.


Функція запису в файл

Запис в деякому сенсі навіть простіше, ніж читання. Необхідно відкрити файл і записати в нього байтовий масив функцією _lwrite (int handle, string buffer, int bytes). Тут handle - файловий покажчик, отриманий функцією _lopen (), параметр buffer - строкова змінна, параметр bytes вказує скільки байт необхідно записати. Після запису файл закривається функцією _lclose (). Розглянемо авторську функцію WriteFile ():

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 ( "Помилка створення файлу", path); return; } Result = _lclose (handle); } Handle = _lopen (path, OF_WRITE); if (handle <0) {Print ( "Помилка відкриття файлу", path); return; } Result = _llseek (handle, 0, 0); if (result <0) {Print ( "Помилка установки покажчика"); return; } Result = _lwrite (handle, buffer, count); if (result <0) Print ( "Не можу записати файл", path, "", count, "байт"); result = _lclose (handle); if (result <0) Print ( "Помилка закриття файлу", path); }

Правда, потрібно проводити перевірку на помилки. Спочатку робиться спроба відкрити файл на запис:

int handle = _lopen (path, OF_WRITE);

(Функція _lopen () викликається з параметром OF_WRITE).

Якщо спроба виявилася невдалою (handle <0), то робиться спроба створити файл з заданим ім'ям:

handle = _lcreat (path, 0);

Якщо ж і ця функція поверне негативний покажчик, то відбувається дострокове завершення функції WriteFile (). Решта код в цій функції зрозумілий без пояснень. Найпростіша функція start () дозволяє перевірити роботу скрипта File_Read_Write.mq4.

int start () {string buffer = ReadFile ( "C: \\ Text.txt"); int count = StringLen (buffer); Print ( "Прочитано байт:", count); WriteFile ( "C: \\ Text2.txt", buffer); return (0); }

Зверніть увагу, що символ зворотної косої межі "\" (back slash) написаний двічі, хоча насправді повинен бути один. Справа в тому, що деякі спецсимволи, такі символ перекладу рядка ( "\ n") або символ табуляції ( "\ t") пишуться з використанням зворотного слеша,. Якщо ви забудете про це, то при написанні шляху в тестовій змінної у вас будуть проблеми під час виконання програми.

Намагайтеся завжди в строковою константі писати не один зворотний слеш, а два що йдуть один за одним. В цьому випадку компілятор однозначно сприйме його правильно.


Все працює, тільки є одна ложка дьогтю - для великих файлів скрипт функція ReadFile () працює дуже довго.


Причина такого повільного зчитування файлу криється в тому, що ми зчитуємо інформацію по одному байту (символу). На малюнку видно, що файл розміром 280 324 байти зажадав 103 секунди. Стільки часу знадобилося щоб 280 324 рази вважати по одному символу. Ви можете самостійно перевірити час роботи скрипта File Read Write.mq4, який прикладений до статті. Як ми можемо прискорити час читання з файлу? Відповідь напрошується сам собою - потрібно зчитувати не по одному символу, а, наприклад, по 50 символів за раз. Тоді кількість звернень до функції _lread () скоротитися в 50 разів. Відповідно, і час читання має скоротитися в 50 разів. Перевіримо це.

Нова функція читання файлу блоками по 50 байт.

Змінюємо код, назвемо новий варіант як xFiles, mq4. Компілюємо і запускаємо його на виконання. Тут необхідно нагадати, що в настройках (Ctrl + O) необхідно дозволити імпорт функцій з DLL.


Отже, час виконання модифікованого скрипта xFiles.mq4 склало 2047 мілісекунд, що становить приблизно 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 ( "Помилка відкриття файлу", path); return ( ""); } Int result = _llseek (handle, 0, 0); if (result <0) {Print ( "Помилка установки покажчика"); 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 ( "Останній прочитаний блок має розмір в байтах:", last); char50 = StringSubstr (char50, 0, last); buffer = buffer + char50; result = _lclose (handle); if (result <0) Print ( "Помилка закриття файлу", path); return (buffer); }

Зверніть увагу, що строкова змінна char50 тепер ініцілізірованна константою в 50 символів (символ "x" і 49 пробілів).

Зверніть увагу, що строкова змінна char50 тепер ініцілізірованна константою в 50 символів (символ x і 49 пробілів)

Тепер ми можемо виробляти читання з файлу таким чином, щоб зчитувати в цю змінну 50 байт (символів) за один раз:

result = _lread (handle, char50, read_size);

Тут read_size = 50. Зрозуміло, що розмір зчитує файлу дуже рідко буде кратний 50 байтам, а значить в якийсь момент результатом виконання цієї функції буде значення, відмінне від 50-ти. Це є сигналом для припинення циклу і останній прочитаний в змінну блок символів усікається до розміру реально прочитаного кількості байтів.

Це є сигналом для припинення циклу і останній прочитаний в змінну блок символів усікається до розміру реально прочитаного кількості байтів

Ви можете змінити розмір буфера для зчитування за допомогою функції lread () до розміру N, тільки не забудьте зробити дві модифікації:

  1. встановіть значення read_size = N;
  2. Ініціалізуйте строкову змінну char50 константою довжини N ( N <256 ).

Операцію читання ми прискорили, залишилася остання задача - обробка помилки з неіснуючим шляхом при спробі запису файлу. У функції WriteFile () проводиться спроба створення файлу, але не обробляється ситуація, коли папка, що містить шлях до імені файлу, відсутня. Значить нам необхідна ще одна функція -

Функція створення папок

Функція для створення папок також доступна в бібліотеці kernel32.dll - CreateDirectory Function . Необхідно тільки відзначити, що ця функція робить спробу створення тільки папки самого нижнього рівня, вона не створює всі проміжні папки в шляху в разі їх відсутності. Наприклад, якщо ми спробуємо за допомогою цієї функції створити папку "З: \ folder_A \ folder_B", то спроба буде успішною в тому випадку, якщо шлях "З: / 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. В цьому випадку створювана папка буде наслідувати всі права від батьківської папки. Але перш ніж спробувати застосувати цю функцію, нам знадобитися зробити таку операцію, як -

Розбір шляху на складові

Справді, нехай нам необхідно створити патку четвертого рівня, наприклад таку:

тут папку folder_D назвемо папкою четвертого рівня, тому що над нею є ще три папки вищих рівнів. Диск "С:" містить папку "folder_A", папка "З: \ folder_A \" містить папку "folder_B", папка "З: \ folder_A \ folder_B \" містить папку "folder_С"; і так далі. Значить нам необхідно розбити весь шлях до файлу на масив вкладених один в одного файлів. Назвемо потрібну функцію як ParsePath ():

bool ParsePath (string & folder [], string path) {bool res = false; int k = StringLen (path); if (k == 0) return (res); k--; Print ( "розпарсити шлях =>", path); int folderNumber = 0; int i = 0; while (k> = 0) {int char = StringGetChar (path, k); if (char == 92) {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 [] іменами знайдених шляхів, починаючи з самого нижнього і закінчуючи самим верхнім. Для нашого прикладу масив folder буде містити наступні значення:

folder [0] = "З: \ folder_A \ folder_B \ folder_С \ folder_D";
folder [1] = "З: \ folder_A \ folder_B \ folder_С";
folder [2] = "З: \ folder_A \ folder_B";
folder [3] = "З: \ folder_A";
folder [4] = "З:";

Якщо нам необхідно записати файл з ім'ям З: \ folder_A \ folder_B \ folder_С \ folder_D \ test.txt ", то зазначений шлях ми можемо розбити на ім'я файлу test.txt і струтури вкладених папок" З: \ folder_A \ folder_B \ folder_С \ folder_D ", яка і містить даний файл. При невдалій спробі створення файлу цим шляхом, в першу чергу необхідно спробувати створити папку самого нижнього рівня" С: \ folder_A \ folder_B \ folder_С \ folder_D ".

Якщо спроба створення даної папки не увінчалася успіхом, то скоріш за все відсутня батьківська папка "З: \ folder_A \ folder_B \ folder_С". Тому ми будемо робити створити папки все більш високого рівня, поки не отримав повідомлення про вдале заверешніі функції CreateDirectoryA (). Ось тому нам і знадобилася функція, яка заповнює строковий масив folder [] іменами папок в зростаючому порядку. Найперший нульовий індекс містить папку самого нижнього рівня, останній індекс масиву містить кореневої каталог.

Тепер ми можемо зібрати і саму функцію, яка створює за вказаною шляху всі необхідні проміжні папки:

bool CreateFullPath (string path) {bool res = false; if (StringLen (path) == 0) return (false); Print ( "Створюємо шлях =>", path); string folders []; if (! ParsePath (folders, path)) return (false); Print ( "Всього вкладених папок:", ArraySize (folders)); int empty []; int i = 0; while (CreateDirectoryA (folders [i], empty) == 0) i ++; Print ( "Створили папку:", folders [i]); i--; while (i> = 0) {CreateDirectoryA (folders [i], empty); Print ( "Створили папку:", folders [i]); i--; } If (i <0) res = true; return (res); }

Тепер залишилося тільки змінити трохи функцію WriteFile () з урахуванням можливості створити нову папку.

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 ( "Помилка створення файлу", path); if (! CreateFullPath (path)) {Print ( "Не вдалося створити папку:", path); return; } Else handle = _lcreat (path, 0); } Result = _lclose (handle); handle = - 1; } If (handle <0) handle = _lopen (path, OF_WRITE); if (handle <0) {Print ( "Помилка відкриття файлу", path); return; } Result = _llseek (handle, 0, 0); if (result <0) {Print ( "Помилка установки покажчика"); return; } Result = _lwrite (handle, buffer, count); if (result <0) Print ( "Не можу записати файл", path, "", count, "байт"); result = _lclose (handle); if (result <0) Print ( "Помилка закриття файлу", path); return; }

Логіка роботи зміненої функції представлена ​​на малюнку.

Логіка роботи зміненої функції представлена ​​на малюнку

Зверніть увагу, що після закриття новоствореного файлу, ми встановлюємо змінну дескриптора файлу handle в від'ємне значення.

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

Це зроблено для того, щоб рядком нижче перевірити значення handle і відкрити файл на читання тільки в тому випадку, якщо перше відкриття було невдалим.

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

Це дозволить нам уникнути ситуації, коли помилково відбуваються множинні відкриття файлів, але закривати їх забувають. У таких випадках операційна система повідомляє про перевищення допустимого максимального числа відкритих файлів і не дозволяє відкривати нові файли.

Змінимо функцію start () для перевірки нових можливостей

int start () {int start = GetTickCount (); string buffer = ReadFile ( "C: \\ Text.txt"); int middle = GetTickCount (); int count = StringLen (buffer); Print ( "Прочитано байт:", count); WriteFile ( "C: \\ folder_A \\ folder_B \\ folder_C \\ folder_D \\ Text2.txt", buffer); int finish = GetTickCount (); Print ( "Розмір файлу -", count, "bytes. Читання:", (middle-start), "ms. Запис:", (finish-middle), "ms."); return (0); }

і запустимо скрипт xFiles.mq4 на виконання.


висновок

Використовувати функції WinAPI не так складно, але не потрібно забувати і про зворотний бік виходу за пісочницю:

Перш ніж запустити незнайому програму в виконуваному вигляді c розширенням ex4 (без вихідного коду на MQL4), яка вимагає права на імпорт функцій із зовнішніх DLL, подумайте гарненько про можливі наслідки.

Перш ніж запустити незнайому програму в виконуваному вигляді c розширенням ex4 (без вихідного коду на MQL4), яка вимагає права на імпорт функцій із зовнішніх DLL, подумайте гарненько про можливі наслідки

Посилання, яка може бути корисною: Робота з функціями Windows API і DLL - http://www.compress.ru/article.aspx?id=11741&iid=457


Як ми можемо прискорити час читання з файлу?
Як же був модифікований код для цього?
Aspx?