Lưu hình ảnh vào tệp: ResourceSave
MQL5 API cho phép bạn ghi một tài nguyên vào tệp BMP bằng hàm ResourceSave
. Hiện tại, framework chỉ hỗ trợ các tài nguyên hình ảnh.
bool ResourceSave(const string resource, const string filename)
Tham số resource
và filename
lần lượt chỉ định tên của tài nguyên và tệp. Tên tài nguyên phải bắt đầu bằng ::
. Tên tệp có thể chứa đường dẫn tương đối so với thư mục MQL5/Files
. Nếu cần, hàm sẽ tạo tất cả các thư mục con trung gian. Nếu tệp được chỉ định đã tồn tại, nó sẽ bị ghi đè.
Hàm trả về true
nếu thành công.
Để kiểm tra hoạt động của hàm này, nên tạo một hình ảnh gốc. Chúng ta có một hình ảnh phù hợp cho việc này.
Trong quá trình nghiên cứu OOP, ở chương Classes và Interfaces, chúng ta đã bắt đầu một loạt ví dụ về các hình dạng đồ họa: từ phiên bản đầu tiên Shapes1.mq5
trong phần Định nghĩa lớp đến phiên bản cuối cùng Shapes6.mq5
trong phần Các loại lồng nhau. Việc vẽ chưa khả dụng cho chúng ta lúc đó, cho đến chương về đối tượng đồ họa, nơi chúng ta đã triển khai trực quan hóa trong script ObjectShapesDraw.mq5
. Bây giờ, sau khi nghiên cứu các tài nguyên đồ họa, đã đến lúc nâng cấp tiếp theo.
Trong phiên bản mới của script ResourceShapesDraw.mq5
, chúng ta sẽ vẽ các hình dạng. Để dễ phân tích các thay đổi so với phiên bản trước, chúng ta giữ nguyên tập hợp hình dạng: hình chữ nhật, hình vuông, hình oval, hình tròn và hình tam giác. Điều này nhằm mục đích làm ví dụ, không phải vì có giới hạn nào trong việc vẽ: ngược lại, có tiềm năng mở rộng tập hợp hình dạng, hiệu ứng hình ảnh và gắn nhãn. Chúng ta sẽ xem xét các tính năng qua một vài ví dụ, bắt đầu từ ví dụ hiện tại. Tuy nhiên, xin lưu ý rằng không thể trình bày toàn bộ phạm vi ứng dụng trong phạm vi cuốn sách này.
Sau khi các hình dạng được tạo và vẽ, chúng ta lưu tài nguyên kết quả vào một tệp.
Cơ sở của hệ thống phân cấp lớp hình dạng là lớp Shape
với phương thức draw
.
class Shape
{
public:
...
virtual void draw() = 0;
...
}
2
3
4
5
6
7
Trong các lớp dẫn xuất, nó được triển khai dựa trên các đối tượng đồ họa, với các lệnh gọi đến ObjectCreate
và thiết lập tiếp theo của các đối tượng bằng các hàm ObjectSet
. Bức tranh chung của việc vẽ như vậy là chính biểu đồ.
Bây giờ chúng ta cần vẽ các pixel trong một tài nguyên chung theo hình dạng cụ thể. Nên phân bổ một tài nguyên chung và các phương thức để sửa đổi pixel trong đó vào một lớp riêng hoặc tốt hơn là một giao diện.
Một thực thể trừu tượng sẽ cho phép chúng ta không tạo liên kết với phương thức tạo và cấu hình tài nguyên. Cụ thể, triển khai tiếp theo của chúng ta sẽ đặt tài nguyên trong một đối tượng OBJ_BITMAP_LABEL
(như đã làm trong chương này), và đối với một số người, việc tạo hình ảnh trong bộ nhớ và lưu vào đĩa mà không cần vẽ lên biểu đồ có thể đủ (vì nhiều nhà giao dịch thích chụp trạng thái biểu đồ định kỳ).
Hãy gọi giao diện là Drawing
.
interface Drawing
{
void point(const float x1, const float y1, const uint pixel);
void line(const int x1, const int y1, const int x2, const int y2, const color clr);
void rect(const int x1, const int y1, const int x2, const int y2, const color clr);
};
2
3
4
5
6
Đây chỉ là ba phương thức vẽ cơ bản nhất, đủ cho trường hợp này.
Phương thức point
là công khai (cho phép đặt một điểm riêng lẻ), nhưng ở một mức độ nào đó, nó là cấp thấp vì tất cả các phương thức khác sẽ được triển khai thông qua nó. Đó là lý do tại sao tọa độ trong nó là thực, và nội dung của pixel là giá trị đã sẵn sàng của kiểu uint
. Điều này sẽ cho phép, nếu cần, áp dụng các thuật toán chống răng cưa để các hình dạng không bị lởm chởm do pixel hóa. Chúng ta sẽ không đề cập đến vấn đề này ở đây.
Xét đến một giao diện, phương thức Shape::draw
biến thành như sau:
virtual void draw(Drawing *drawing) = 0;
Sau đó, trong lớp Rectangle
, việc vẽ hình chữ nhật được giao phó cho giao diện mới một cách rất dễ dàng.
class Rectangle : public Shape
{
protected:
int dx, dy; // kích thước (chiều rộng, chiều cao)
...
public:
void draw(Drawing *drawing) override
{
// x, y - điểm neo (trung tâm) trong Shape
drawing.rect(x — dx / 2, y — dy / 2, x + dx / 2, y + dy / 2, backgroundColor);
}
};
2
3
4
5
6
7
8
9
10
11
12
Cần nhiều nỗ lực hơn để vẽ một hình elip.
class Ellipse : public Shape
{
protected:
int dx, dy; // bán kính lớn và nhỏ
...
public:
void draw(Drawing *drawing) override
{
// (x, y) - trung tâm
const int hh = dy * dy;
const int ww = dx * dx;
const int hhww = hh * ww;
int x0 = dx;
int step = 0;
// đường kính ngang chính
drawing.line(x - dx, y, x + dx, y, backgroundColor);
// các đường ngang ở nửa trên và dưới, giảm dần đối xứng về độ dài
for(int j = 1; j <= dy; j++)
{
for(int x1 = x0 - (step - 1); x1 > 0; --x1)
{
if(x1 * x1 * hh + j * j * ww <= hhww)
{
step = x0 - x1;
break;
}
}
x0 -= step;
drawing.line(x - x0, y - j, x + x0, y - j, backgroundColor);
drawing.line(x - x0, y + j, x + x0, y + j, backgroundColor);
}
}
};
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
Cuối cùng, đối với hình tam giác, việc vẽ được triển khai như sau.
class Triangle: public Shape
{
protected:
int dx; // một kích thước, vì tam giác là đều
...
public:
virtual void draw(Drawing *drawing) override
{
// (x, y) - trung tâm
// R = a * sqrt(3) / 3
// p0: x, y + R
// p1: x - R * cos(30), y - R * sin(30)
// p2: x + R * cos(30), y - R * sin(30)
// Chiều cao Pythagore: dx * dx = dx * dx / 4 + h * h
// sqrt(dx * dx * 3/4) = h
const double R = dx * sqrt(3) / 3;
const double H = sqrt(dx * dx * 3 / 4);
const double angle = H / (dx / 2);
// đường thẳng đứng chính (chiều cao tam giác)
const int base = y + (int)(R - H);
drawing.line(x, y + (int)R, x, base, backgroundColor);
...
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Tiếp theo, chúng ta mô tả đối tượng của lớp MyDrawing
, tạo một số lượng hình dạng ngẫu nhiên được xác định trước (mọi thứ vẫn không thay đổi ở đây, bao gồm trình tạo addRandomShape
và macro FIGURES
bằng 21), vẽ chúng trong tài nguyên và hiển thị chúng trong đối tượng trên biểu đồ.
MyDrawing raster;
for(int i = 0; i < FIGURES; ++i)
{
raster.push(addRandomShape());
}
raster.draw(); // hiển thị trạng thái ban đầu
...
2
3
4
5
6
7
8
9
Trong ví dụ ObjectShapesDraw.mq5
, chúng ta đã bắt đầu một vòng lặp vô hạn để di chuyển các mảnh một cách ngẫu nhiên. Hãy lặp lại thủ thuật này ở đây. Chúng ta sẽ cần thêm lớp MyDrawing
vì mảng các hình dạng được lưu trữ bên trong nó. Hãy viết một phương thức đơn giản shake
.
class MyDrawing: public Drawing
{
public:
...
void shake()
{
ArrayInitialize(data, bg);
for(int i = 0; i < ArraySize(shapes); ++i)
{
shapes[i].move(random(20) - 10, random(20) - 10);
}
}
...
};
2
3
4
5
6
7
8
9
10
11
12
13
14
Sau đó, trong OnStart
, chúng ta có thể sử dụng phương thức mới trong một vòng lặp cho đến khi người dùng dừng hoạt hình.
void OnStart()
{
...
while(!IsStopped())
{
Sleep(250);
raster.shake();
raster.draw();
}
...
}
2
3
4
5
6
7
8
9
10
11
Tại thời điểm này, chức năng của ví dụ trước hầu như được lặp lại. Nhưng chúng ta cần thêm việc lưu hình ảnh vào tệp. Vì vậy, hãy thêm tham số đầu vào SaveImage
.
input bool SaveImage = false;
Khi nó được đặt thành true
, kiểm tra hiệu suất của hàm ResourceSave
.
void OnStart()
{
...
if(SaveImage)
{
const string filename = "temp.bmp";
if(ResourceSave(raster.resource(), filename))
{
Print("Bitmap image saved: ", filename);
}
else
{
Print("Can't save image ", filename, ", ", E2S(_LastError));
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Ngoài ra, vì chúng ta đang nói về các biến đầu vào, hãy để người dùng chọn nền và truyền giá trị kết quả vào hàm tạo MyDrawing
.
input color BackgroundColor = clrNONE;
void OnStart()
{
...
MyDrawing raster(BackgroundColor != clrNONE ? ColorToARGB(BackgroundColor) : 0);
...
}
2
3
4
5
6
7
Vậy là mọi thứ đã sẵn sàng cho bài kiểm tra đầu tiên.
Nếu bạn chạy script ResourceShapesDraw.mq5
, biểu đồ sẽ tạo ra một hình ảnh như sau.
Bitmap của tài nguyên với tập hợp các hình dạng ngẫu nhiên
Khi so sánh hình ảnh này với những gì chúng ta thấy trong ví dụ ObjectShapesDraw.mq5
, hóa ra cách vẽ mới của chúng ta có phần khác biệt so với cách terminal hiển thị các đối tượng. Mặc dù các hình dạng và màu sắc là chính xác, nhưng những nơi các hình dạng chồng lên nhau được biểu thị theo cách khác.
Script của chúng ta vẽ các hình dạng với màu sắc được chỉ định, chồng chúng lên nhau theo thứ tự xuất hiện trong mảng. Các hình dạng sau đè lên các hình dạng trước. Trong khi đó, terminal áp dụng một số loại pha trộn màu (đảo ngược) ở những nơi chồng lấp.
Cả hai phương pháp đều có quyền tồn tại, không có lỗi ở đây. Tuy nhiên, liệu có thể đạt được hiệu ứng tương tự khi vẽ không?
Chúng ta có toàn quyền kiểm soát quá trình vẽ, vì vậy bất kỳ hiệu ứng nào cũng có thể được áp dụng, không chỉ hiệu ứng từ terminal.
Ngoài cách vẽ đơn giản ban đầu, hãy triển khai thêm một vài chế độ nữa. Tất cả đều được tổng hợp trong liệt kê COLOR_EFFECT
.
enum COLOR_EFFECT
{
PLAIN, // vẽ đơn giản với chồng lấp (mặc định)
COMPLEMENT, // vẽ với màu bổ sung (như trong terminal)
BLENDING_XOR, // pha trộn màu với XOR '^'
DIMMING_SUM, // "làm tối" màu với '+'
LIGHTEN_OR, // "làm sáng" màu với '|'
};
2
3
4
5
6
7
8
Hãy thêm một biến đầu vào để chọn chế độ.
input COLOR_EFFECT ColorEffect = PLAIN;
Hãy hỗ trợ các chế độ trong lớp MyDrawing
. Trước tiên, hãy mô tả trường và phương thức tương ứng.
class MyDrawing: public Drawing
{
...
COLOR_EFFECT xormode;
...
public:
void setColorEffect(const COLOR_EFFECT x)
{
xormode = x;
}
...
2
3
4
5
6
7
8
9
10
11
Sau đó, chúng ta cải thiện phương thức point
.
virtual void point(const float x1, const float y1, const uint pixel) override
{
...
if(index >= 0 && index < ArraySize(data))
{
switch(xormode)
{
case COMPLEMENT:
data[index] = (pixel ^ (1 - data[index])); // pha trộn với màu bổ sung
break;
case BLENDING_XOR:
data[index] = (pixel & 0xFF000000) | (pixel ^ data[index]); // pha trộn trực tiếp (XOR)
break;
case DIMMING_SUM:
data[index] = (pixel + data[index]); // "làm tối" (SUM)
break;
case LIGHTEN_OR:
data[index] = (pixel & 0xFF000000) | (pixel | data[index]); // "làm sáng" (OR)
break;
case PLAIN:
default:
data[index] = pixel;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Bạn có thể thử chạy script ở các chế độ khác nhau và so sánh kết quả. Đừng quên khả năng tùy chỉnh nền. Dưới đây là ví dụ về hiệu ứng làm sáng.
Hình ảnh các hình dạng với pha trộn màu làm sáng
Để thấy rõ sự khác biệt về hiệu ứng, bạn có thể tắt ngẫu nhiên màu và di chuyển hình dạng. Cách chồng lấp đối tượng tiêu chuẩn tương ứng với hằng số COMPLEMENT
.
Để thử nghiệm cuối cùng, bật tùy chọn SaveImage
. Trong trình xử lý OnStart
, khi tạo tên tệp cho hình ảnh, chúng ta giờ đây sử dụng tên của chế độ hiện tại. Chúng ta cần lấy một bản sao của hình ảnh trên biểu đồ vào tệp.
...
if(SaveImage)
{
const string filename = EnumToString(ColorEffect) + ".bmp";
if(ResourceSave(raster.resource(), filename))
...
2
3
4
5
6
Đối với các cấu trúc đồ họa phức tạp hơn của giao diện của chúng ta, Drawing
có thể không đủ. Do đó, bạn có thể sử dụng các lớp vẽ sẵn được cung cấp cùng với MetaTrader 5 hoặc có sẵn trong codebase mql5.com. Đặc biệt, hãy xem tệp MQL5/Include/Canvas/Canvas.mqh
.