Đọc và sửa đổi dữ liệu tài nguyên: ResourceReadImage
Hàm ResourceReadImage
cho phép đọc dữ liệu của tài nguyên được tạo bởi hàm ResourceCreate
hoặc được nhúng vào tệp thực thi tại thời điểm biên dịch theo chỉ thị #resource
. Mặc dù có hậu tố "Image" trong tên, hàm này hoạt động với bất kỳ mảng dữ liệu nào, bao gồm cả các mảng tùy chỉnh (xem ví dụ về Reservoir.mq5
dưới đây).
bool ResourceReadImage(const string resource, uint &data[], uint &width, uint &height)
Tên tài nguyên được chỉ định trong tham số resource
. Để truy cập tài nguyên của chính bạn, dạng ngắn ::resource_name
là đủ. Để đọc tài nguyên từ một tệp biên dịch khác, bạn cần tên đầy đủ kèm theo đường dẫn theo quy tắc phân giải đường dẫn được mô tả trong phần về tài nguyên. Cụ thể, đường dẫn bắt đầu bằng dấu gạch chéo ngược có nghĩa là đường dẫn từ thư mục gốc MQL5 (theo cách này, \\path\\filename.ex5::resource_name
được tìm kiếm trong tệp /MQL5/path/filename.ex5
dưới tên resource_name
), và đường dẫn không có ký tự đầu này có nghĩa là đường dẫn tương đối so với thư mục nơi chương trình thực thi đang nằm.
Thông tin bên trong của tài nguyên sẽ được ghi vào mảng nhận data
, và các tham số width
và height
sẽ lần lượt nhận được chiều rộng và chiều cao, tức là kích thước của mảng (width*height
) một cách gián tiếp. Riêng biệt, width
và height
chỉ có ý nghĩa nếu hình ảnh được lưu trữ trong tài nguyên. Mảng phải là động hoặc cố định, nhưng có kích thước đủ lớn. Nếu không, chúng ta sẽ nhận được lỗi SMALL_ARRAY (5052).
Nếu trong tương lai bạn muốn tạo một tài nguyên đồ họa dựa trên mảng data
, thì tài nguyên nguồn nên sử dụng định dạng màu COLOR_FORMAT_ARGB_NORMALIZE
hoặc COLOR_FORMAT_XRGB_NOALPHA
. Nếu mảng data
chứa dữ liệu ứng dụng tùy ý, hãy sử dụng COLOR_FORMAT_XRGB_NOALPHA
.
Ví dụ đầu tiên, hãy xem xét kịch bản ResourceReadImage.mq5
. Nó thể hiện một số khía cạnh của việc làm việc với tài nguyên đồ họa:
- Tạo một tài nguyên hình ảnh từ tệp bên ngoài
- Đọc và sửa đổi dữ liệu của hình ảnh này trong một tài nguyên được tạo động khác
- Lưu giữ các tài nguyên đã tạo trong bộ nhớ terminal giữa các lần chạy kịch bản
- Sử dụng tài nguyên trong các đối tượng trên biểu đồ
- Xóa một đối tượng và tài nguyên
Việc sửa đổi hình ảnh trong trường hợp cụ thể này có nghĩa là đảo ngược tất cả các màu (dễ nhìn thấy nhất).
Tất cả các phương pháp làm việc trên được thực hiện trong ba giai đoạn: mỗi giai đoạn được thực hiện trong một lần chạy kịch bản. Kịch bản xác định giai đoạn hiện tại bằng cách phân tích các tài nguyên và đối tượng có sẵn:
- Khi không có tài nguyên đồ họa cần thiết, kịch bản sẽ tạo chúng (một hình ảnh gốc và một hình ảnh đảo ngược).
- Nếu có tài nguyên nhưng không có đối tượng đồ họa, kịch bản sẽ tạo một đối tượng với hai hình ảnh từ bước đầu tiên cho trạng thái bật/tắt (có thể chuyển đổi bằng cách nhấp chuột).
- Nếu đã có đối tượng, kịch bản sẽ xóa đối tượng và tài nguyên.
Hàm chính của kịch bản bắt đầu bằng cách xác định tên của các tài nguyên và đối tượng trên biểu đồ.
void OnStart()
{
const static string resource = "::Images\\pseudo.bmp";
const static string inverted = resource + "_inv";
const static string object = "object";
...
2
3
4
5
6
Lưu ý rằng chúng ta đã chọn một tên cho tài nguyên gốc trông giống như vị trí của tệp bmp
trong thư mục Images
tiêu chuẩn, nhưng không có tệp như vậy. Điều này nhấn mạnh bản chất ảo của tài nguyên và cho phép bạn thực hiện thay thế để đáp ứng yêu cầu kỹ thuật hoặc để làm khó việc đảo ngược kỹ thuật chương trình của bạn.
Lần gọi ResourceReadImage
tiếp theo được sử dụng để kiểm tra xem tài nguyên đã tồn tại chưa. Trong trạng thái ban đầu (lần chạy đầu tiên), chúng ta sẽ nhận được kết quả âm (false
) và bắt đầu bước đầu tiên: chúng ta tạo tài nguyên gốc từ tệp \\Images\\dollar.bmp
, sau đó đảo ngược nó trong một tài nguyên mới với hậu tố _inv
.
uint data[], width, height;
// check for resource existence
if(!PRTF(ResourceReadImage(resource, data, width, height)))
{
Print("Initial state: Creating 2 bitmaps");
PRTF(ResourceCreate(resource, "\\Images\\dollar.bmp")); // try "argb.bmp"
ResourceCreateInverted(resource, inverted);
}
...
2
3
4
5
6
7
8
9
Mã nguồn của hàm trợ giúp ResourceCreateInverted
sẽ được trình bày dưới đây.
Nếu tài nguyên được tìm thấy (lần chạy thứ hai), kịch bản kiểm tra sự tồn tại của đối tượng và, nếu cần, tạo nó, bao gồm thiết lập các thuộc tính với tài nguyên hình ảnh trong hàm ShowBitmap
(xem bên dưới).
else
{
Print("Resources (bitmaps) are detected");
if(PRTF(ObjectFind(0, object) < 0))
{
Print("Active state: Creating object to draw 2 bitmaps");
ShowBitmap(object, resource, inverted);
}
...
2
3
4
5
6
7
8
9
Nếu cả tài nguyên và đối tượng đã có trên biểu đồ, thì chúng ta đang ở giai đoạn cuối và phải xóa tất cả tài nguyên.
else
{
Print("Cleanup state: Removing object and resources");
PRTF(ObjectDelete(0, object));
PRTF(ResourceFree(resource));
PRTF(ResourceFree(inverted));
}
}
}
2
3
4
5
6
7
8
9
Hàm ResourceCreateInverted
sử dụng lệnh gọi ResourceReadImage
để lấy mảng pixel và sau đó đảo ngược màu trong chúng bằng toán tử ^
(XOR) và một toán hạng với tất cả các bit đơn trong các thành phần màu.
bool ResourceCreateInverted(const string resource, const string inverted)
{
uint data[], width, height;
PRTF(ResourceReadImage(resource, data, width, height));
for(int i = 0; i < ArraySize(data); ++i)
{
data[i] = data[i] ^ 0x00FFFFFF;
}
return PRTF(ResourceCreate(inverted, data, width, height, 0, 0, 0,
COLOR_FORMAT_ARGB_NORMALIZE));
}
2
3
4
5
6
7
8
9
10
11
Mảng data
mới được chuyển đến ResourceCreate
để tạo hình ảnh thứ hai.
Hàm ShowBitmap
tạo một đối tượng đồ họa theo cách thông thường (ở góc dưới bên phải của biểu đồ) và thiết lập các thuộc tính của nó cho trạng thái bật và tắt tương ứng với hình ảnh gốc và hình ảnh đảo ngược.
void ShowBitmap(const string name, const string resourceOn, const string resourceOff = NULL)
{
ObjectCreate(0, name, OBJ_BITMAP_LABEL, 0, 0, 0);
ObjectSetString(0, name, OBJPROP_BMPFILE, 0, resourceOn);
if(resourceOff != NULL) ObjectSetString(0, name, OBJPROP_BMPFILE, 1, resourceOff);
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, 50);
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, 50);
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_RIGHT_LOWER);
ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_RIGHT_LOWER);
}
2
3
4
5
6
7
8
9
10
11
Vì đối tượng mới tạo mặc định là tắt, chúng ta sẽ thấy hình ảnh đảo ngược trước và có thể chuyển sang hình ảnh gốc bằng cách nhấp chuột. Nhưng hãy nhớ rằng kịch bản của chúng ta thực hiện các hành động từng bước, do đó, trước khi hình ảnh xuất hiện trên biểu đồ, kịch bản phải được chạy hai lần. Ở tất cả các giai đoạn, trạng thái hiện tại và các hành động thực hiện (cùng với chỉ báo thành công hoặc lỗi) được ghi lại.
Sau lần chạy đầu tiên, các mục sau sẽ xuất hiện trong nhật ký:
ResourceReadImage(resource,data,width,height)=false / RESOURCE_NOT_FOUND(4016)
Initial state: Creating 2 bitmaps
ResourceCreate(resource,\Images\dollar.bmp)=true / ok
ResourceReadImage(resource,data,width,height)=true / ok
ResourceCreate(inverted,data,width,height,0,0,0,COLOR_FORMAT_XRGB_NOALPHA)=true / ok
2
3
4
5
Nhật ký cho biết tài nguyên chưa được tìm thấy và đó là lý do kịch bản đã tạo chúng. Sau lần chạy thứ hai, nhật ký sẽ cho biết tài nguyên đã được tìm thấy (được để lại trong bộ nhớ từ lần chạy trước của kịch bản) nhưng đối tượng chưa có, và kịch bản sẽ tạo nó dựa trên các tài nguyên.
ResourceReadImage(resource,data,width,height)=true / ok
Resources (bitmaps) are detected
ObjectFind(0,object)<0=true / OBJECT_NOT_FOUND(4202)
Active state: Creating object to draw 2 bitmaps
2
3
4
Chúng ta sẽ thấy một đối tượng và một hình ảnh trên biểu đồ. Chuyển đổi trạng thái có thể thực hiện bằng cách nhấp chuột (sự kiện về thay đổi trạng thái không được xử lý ở đây).
Hình ảnh đảo ngược và gốc trong một đối tượng trên biểu đồ
Cuối cùng, trong lần chạy thứ ba, kịch bản sẽ phát hiện đối tượng và xóa tất cả các phát triển của nó.
ResourceReadImage(resource,data,width,height)=true / ok
Resources (bitmaps) are detected
ObjectFind(0,object)<0=false / ok
Cleanup state: Removing object and resources
ObjectDelete(0,object)=true / ok
ResourceFree(resource)=true / ok
ResourceFree(inverted)=true / ok
2
3
4
5
6
7
Sau đó, bạn có thể lặp lại chu kỳ.
Ví dụ thứ hai của phần này sẽ xem xét việc sử dụng tài nguyên để lưu trữ dữ liệu ứng dụng tùy ý, tức là một loại bảng nhớ tạm bên trong terminal (về lý thuyết, có thể có bất kỳ số lượng bộ đệm nào như vậy, vì mỗi bộ đệm là một tài nguyên có tên riêng). Do tính phổ quát của vấn đề, chúng ta sẽ tạo lớp Reservoir
với chức năng chính (trong tệp Reservoir.mqh
), và dựa trên đó, chúng ta sẽ viết một kịch bản demo (Reservoir.mq5
).
Trước khi đi sâu vào Reservoir
, hãy giới thiệu một liên kết phụ trợ ByteOverlay
sẽ thường xuyên cần thiết. Một liên kết sẽ cho phép bất kỳ loại tích hợp đơn giản nào (bao gồm cả cấu trúc đơn giản) được chuyển đổi thành mảng byte và ngược lại. "Đơn giản" ở đây nghĩa là tất cả các loại số tích hợp, ngày và giờ, liệt kê, màu sắc và cờ boolean. Tuy nhiên, các đối tượng và mảng động không còn đơn giản và sẽ không được kho lưu trữ mới của chúng ta hỗ trợ (do giới hạn kỹ thuật của nền tảng). Chuỗi cũng không được coi là đơn giản, nhưng đối với chúng, chúng ta sẽ tạo một ngoại lệ và xử lý chúng theo cách đặc biệt.
template<typename T>
union ByteOverlay
{
uchar bytes[sizeof(T)];
T value;
ByteOverlay() : value(0) {}
ByteOverlay(const T v) : value(v) {}
ByteOverlay(const uchar &array[], const int pos) { ArrayCopy(bytes, array, 0, pos, sizeof(T)); }
}
2
3
4
5
6
7
8
9
10
Lớp Reservoir
được thiết kế để đóng gói dữ liệu ứng dụng thành một mảng storage
bên trong và ánh xạ nó tới tài nguyên hoặc ngược lại.
class Reservoir
{
uint storage[];
int offset;
public:
Reservoir() : offset(0) {}
template<typename T>
int packArray(const T &input[])
{
const int n = ArraySize(input);
const int bytesize = n * sizeof(T);
uchar bytes[];
ArrayResize(bytes, bytesize);
for(int i = 0; i < n; ++i)
{
ByteOverlay<T> overlay(input[i]);
ArrayCopy(bytes, overlay.bytes, i * sizeof(T), 0, sizeof(T));
}
return packArray(bytes);
}
int packString(const string input)
{
uchar bytes[];
StringToCharArray(input, bytes, 0, -1, CP_UTF8);
return packArray(bytes);
}
template<typename T>
int packNumber(const T number)
{
T array[1] = {number};
return packArray(array);
}
int packArray(const uchar &input[])
{
const int n = ArraySize(input);
const int oldsize = ArraySize(storage);
ArrayResize(storage, oldsize + 1 + bytesize / sizeof(uint) + (bool)(bytesize % sizeof(uint)));
storage[offset] = n;
for(int i = 0; i < n; ++i)
{
ByteOverlay<uchar> overlay(input[i]);
ArrayCopy(storage, overlay.bytes, offset + 1 + i * sizeof(uchar) / sizeof(uint), 0, sizeof(uchar));
}
offset += 1 + bytesize / sizeof(uint) + (bool)(bytesize % sizeof(uint));
return offset;
}
template<typename T>
int unpackArray(T &output[])
{
uchar bytes[];
const int p = unpackArray(bytes);
if(p == offset) return offset;
const int n = ArraySize(bytes);
ArrayResize(output, n);
for(int i = 0; i < n; ++i)
{
ByteOverlay<T> overlay(bytes, i * sizeof(T));
output[i] = overlay.value;
}
offset += 1 + bytesize / sizeof(uint) + (bool)(bytesize % sizeof(uint));
return offset;
}
int unpackString(string &output)
{
uchar bytes[];
const int p = unpackArray(bytes);
if(p == offset)
{
output = CharArrayToString(bytes, 0, -1, CP_UTF8);
}
return p;
}
template<typename T>
int unpackNumber(T &number)
{
T array[1] = {};
const int p = unpackArray(array);
number = array[0];
return p;
}
int size() const
{
return ArraySize(storage);
}
int cursor() const
{
return offset;
}
void clear()
{
ArrayFree(storage);
offset = 0;
}
bool submit(const string resource)
{
return ResourceCreate(resource, storage, ArraySize(storage), 1, 0, 0, 0, COLOR_FORMAT_XRGB_NOALPHA);
}
bool acquire(const string resource)
{
uint width, height;
if(ResourceReadImage(resource, storage, width, height))
{
return true;
}
return false;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
Việc giải nén chuỗi và số được thực hiện bằng cách gọi unpackArray
.
Chúng ta sẽ trình bày trong kịch bản Reservoir.mq5
, cách sử dụng nó.
Trong nửa đầu của OnStart
, chúng ta mô tả tên cho tài nguyên lưu trữ và đối tượng lớp Reservoir
, sau đó lần lượt "đóng gói" vào đối tượng này một chuỗi, cấu trúc MqlTick
, và số double
. Cấu trúc được "bọc" trong một mảng một phần tử để thể hiện rõ ràng phương thức packArray
. Ngoài ra, chúng ta sẽ cần so sánh dữ liệu được khôi phục với dữ liệu gốc, và MQL5 không cung cấp toán tử ==
cho cấu trúc. Do đó, sẽ thuận tiện hơn khi sử dụng hàm ArrayCompare
.
#include <MQL5Book/Reservoir.mqh>
#include <MQL5Book/PRTF.mqh>
void OnStart()
{
const string resource = "::reservoir";
Reservoir res1;
string message = "message1"; // chuỗi để ghi vào tài nguyên
PRTF(res1.packString(message));
MqlTick tick1[1]; // thêm một cấu trúc đơn giản
SymbolInfoTick(_Symbol, tick1[0]);
PRTF(res1.packArray(tick1));
PRTF(res1.packNumber(DBL_MAX)); // số thực
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Khi tất cả dữ liệu cần thiết đã được "đóng gói" vào đối tượng, ghi nó vào tài nguyên và xóa đối tượng.
res1.submit(resource); // tạo một tài nguyên với dữ liệu lưu trữ
res1.clear(); // xóa đối tượng, nhưng không xóa tài nguyên
2
Trong nửa sau của OnStart
, chúng ta thực hiện các thao tác ngược lại của việc đọc dữ liệu từ tài nguyên.
string reply; // biến mới cho thông điệp
MqlTick tick2[1]; // cấu trúc mới cho tick
double result; // biến mới cho số
PRTF(res1.acquire(resource)); // kết nối đối tượng với tài nguyên đã cho
PRTF(res1.unpackString(reply)); // đọc chuỗi
PRTF(res1.unpackArray(tick2)); // đọc cấu trúc đơn giản
PRTF(res1.unpackNumber(result));// đọc số
// xuất và so sánh dữ liệu từng phần tử
PRTF(reply);
PRTF(ArrayCompare(tick1, tick2));
ArrayPrint(tick2);
PRTF(result == DBL_MAX);
// đảm bảo kho lưu trữ được đọc hoàn toàn
PRTF(res1.size());
PRTF(res1.cursor());
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Cuối cùng, chúng ta dọn dẹp tài nguyên, vì đây là một bài kiểm tra. Trong các tác vụ thực tế, một chương trình MQL có thể sẽ để lại tài nguyên đã tạo trong bộ nhớ để các chương trình khác có thể đọc được. Trong hệ thống phân cấp đặt tên, tài nguyên được khai báo lồng trong chương trình đã tạo ra chúng. Do đó, để truy cập từ các chương trình khác, bạn phải chỉ định tên của tài nguyên cùng với tên của chương trình và tùy chọn đường dẫn (nếu chương trình tạo và chương trình đọc nằm trong các thư mục khác nhau). Ví dụ, để đọc tài nguyên mới tạo từ bên ngoài, đường dẫn đầy đủ \\Scripts\\MQL5Book\\p7\\Reservoir.ex5::reservoir
sẽ thực hiện được công việc.
PrintFormat("Cleaning up local storage '%s'", resource);
ResourceFree(resource);
}
2
3
Vì tất cả các lệnh gọi phương thức chính được kiểm soát bởi macro PRTF
, khi chúng ta chạy kịch bản, chúng ta sẽ thấy một "báo cáo" tiến độ chi tiết trong nhật ký.
res1.packString(message)=4 / ok
res1.packArray(tick1)=20 / ok
res1.packNumber(DBL_MAX)=23 / ok
res1.acquire(resource)=true / ok
res1.unpackString(reply)=4 / ok
res1.unpackArray(tick2)=20 / ok
res1.unpackNumber(result)=23 / ok
reply=message1 / ok
ArrayCompare(tick1,tick2)=0 / ok
[time] [bid] [ask] [last] [volume] [time_msc] [flags] [volume_real]
[0] 2022.05.19 23:09:32 1.05867 1.05873 0.0000 0 1653001772050 6 0.00000
result==DBL_MAX=true / ok
res1.size()=23 / ok
res1.cursor()=23 / ok
Cleaning up local storage '::reservoir'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Dữ liệu đã được sao chép thành công vào tài nguyên và sau đó được khôi phục từ đó.
Các chương trình có thể sử dụng phương pháp này để trao đổi dữ liệu khối lượng lớn không phù hợp với các thông điệp tùy chỉnh (sự kiện CHARTEVENT_CUSTOM+
). Chỉ cần gửi tên của tài nguyên để đọc trong tham số chuỗi sparam
. Để gửi lại dữ liệu, hãy tạo tài nguyên của riêng bạn với nó và gửi một thông điệp phản hồi.