Xác định chiều rộng và chiều cao của đối tượng
Một số loại đối tượng cho phép bạn đặt kích thước của chúng theo pixel. Những loại này bao gồm OBJ_BUTTON
, OBJ_CHART
, OBJ_BITMAP
, OBJ_BITMAP_LABEL
, OBJ_EDIT
, và OBJ_RECTANGLE_LABEL
. Ngoài ra, các đối tượng OBJ_LABEL
hỗ trợ đọc (nhưng không đặt) kích thước vì nhãn tự động mở rộng hoặc co lại để phù hợp với văn bản mà chúng chứa. Việc cố gắng truy cập các thuộc tính trên các loại đối tượng khác sẽ dẫn đến lỗi OBJECT_WRONG_PROPERTY
(4203).
Định danh | Mô tả |
---|---|
OBJPROP_XSIZE | Chiều rộng của đối tượng theo trục X tính bằng pixel |
OBJPROP_YSIZE | Chiều cao của đối tượng theo trục Y tính bằng pixel |
Cả hai kích thước đều là số nguyên và do đó được xử lý bởi các hàm ObjectGetInteger/ObjectSetInteger
.
Việc xử lý kích thước đặc biệt được thực hiện cho các đối tượng OBJ_BITMAP
và OBJ_BITMAP_LABEL
.
Nếu không gán hình ảnh, các đối tượng này cho phép bạn đặt kích thước tùy ý. Đồng thời, chúng được vẽ trong suốt (chỉ khung viền hiển thị nếu nó không bị "ẩn" bằng cách đặt màu clrNone
), nhưng chúng nhận tất cả các sự kiện, đặc biệt là về chuyển động chuột (kèm theo mô tả văn bản, nếu có, trong tooltip) và các lần nhấp chuột trên đối tượng.
Khi một hình ảnh được gán, nó mặc định theo chiều cao và chiều rộng của đối tượng. Tuy nhiên, một chương trình MQL có thể đặt kích thước nhỏ hơn và chọn một phần của hình ảnh để hiển thị; chi tiết hơn trong phần về khung hình. Nếu bạn cố gắng đặt chiều cao hoặc chiều rộng lớn hơn kích thước hình ảnh, nó sẽ ngừng hiển thị, và kích thước của đối tượng không thay đổi.
Ví dụ, hãy phát triển một phiên bản cải tiến của script ObjectAnchorLabel.mq5
từ phần có tiêu đề Xác định điểm neo trên đối tượng. Trong phần đó, chúng ta di chuyển nhãn văn bản quanh cửa sổ và đảo ngược nó khi chạm đến bất kỳ biên nào của cửa sổ, nhưng chúng ta chỉ tính đến điểm neo. Vì điều này, tùy thuộc vào vị trí của điểm neo trên đối tượng, có thể xảy ra tình huống nhãn gần như hoàn toàn vượt ra ngoài cửa sổ. Chẳng hạn, nếu điểm neo ở phía bên phải của đối tượng, việc di chuyển sang trái sẽ khiến hầu hết văn bản vượt ra ngoài biên trái của cửa sổ trước khi điểm neo chạm vào cạnh.
Trong script mới ObjectSizeLabel.mq5
, chúng ta sẽ tính đến kích thước của đối tượng và thay đổi hướng di chuyển ngay khi nó chạm vào cạnh cửa sổ bằng bất kỳ mặt nào của nó.
Để thực hiện chế độ này một cách chính xác, cần lưu ý rằng mỗi góc cửa sổ được dùng làm tâm tham chiếu tọa độ đến điểm neo trên đối tượng xác định hướng đặc trưng của cả trục X và Y. Ví dụ, nếu người dùng chọn góc trên bên trái trong biến đầu vào ENUM_BASE_CORNER Corner
, thì X tăng từ trái sang phải và Y tăng từ trên xuống dưới. Nếu tâm được coi là góc dưới bên phải, thì X tăng từ phải sang trái của nó, và Y tăng từ dưới lên trên.
Sự kết hợp lẫn nhau khác nhau giữa góc neo trong cửa sổ và điểm neo trên đối tượng đòi hỏi các điều chỉnh khác nhau về khoảng cách giữa các cạnh đối tượng và biên cửa sổ. Cụ thể, khi một trong các góc bên phải và một trong các điểm neo ở phía bên phải của đối tượng được chọn, thì không cần điều chỉnh ở biên phải của cửa sổ, còn ở phía đối diện, bên trái, chúng ta phải tính đến chiều rộng của đối tượng (để kích thước của nó không vượt ra ngoài cửa sổ sang bên trái).
Quy tắc này về việc điều chỉnh kích thước của đối tượng có thể được khái quát hóa:
- Ở biên cửa sổ liền kề với góc neo, cần điều chỉnh khi điểm neo nằm ở phía xa của đối tượng so với góc này;
- Ở biên cửa sổ đối diện với góc neo, cần điều chỉnh khi điểm neo nằm ở phía gần của đối tượng so với góc này.
Nói cách khác, nếu tên của góc (trong phần tử ENUM_BASE_CORNER
) và điểm neo (trong phần tử ENUM_ANCHOR_POINT
) chứa một từ chung (ví dụ, RIGHT), thì cần điều chỉnh ở phía xa của cửa sổ (tức là xa góc đã chọn). Nếu các hướng đối lập được tìm thấy trong sự kết hợp của các mặt ENUM_BASE_CORNER
và ENUM_ANCHOR_POINT
(ví dụ, LEFT và RIGHT), thì cần điều chỉnh ở phía gần của cửa sổ. Các quy tắc này hoạt động tương tự cho trục ngang và dọc.
Ngoài ra, cần lưu ý rằng điểm neo có thể nằm ở giữa bất kỳ cạnh nào của đối tượng. Khi đó, theo hướng vuông góc, cần một khoảng lùi từ biên cửa sổ bằng nửa kích thước của đối tượng.
Trường hợp đặc biệt là điểm neo ở trung tâm của đối tượng. Đối với nó, luôn cần có một khoảng cách lùi theo bất kỳ hướng nào, bằng nửa kích thước của đối tượng.
Logic được mô tả được triển khai trong một hàm đặc biệt gọi là GetMargins
. Nó nhận vào các đầu vào là góc và điểm neo đã chọn, cũng như kích thước của đối tượng (dx
và dy
). Hàm trả về một cấu trúc với 4 trường chứa kích thước của các khoảng lùi bổ sung cần được dành ra từ điểm neo theo hướng của các biên gần và xa của cửa sổ để đối tượng không bị khuất tầm nhìn. Các khoảng lùi dự trữ khoảng cách theo kích thước và vị trí tương đối của chính đối tượng.
struct Margins
{
int nearX; // Tăng X giữa điểm đối tượng và biên cửa sổ liền kề với góc
int nearY; // Tăng Y giữa điểm đối tượng và biên cửa sổ liền kề với góc
int farX; // Tăng X giữa điểm đối tượng và góc đối diện của biên cửa sổ
int farY; // Tăng Y giữa điểm đối tượng và góc đối diện của biên cửa sổ
};
Margins GetMargins(const ENUM_BASE_CORNER corner, const ENUM_ANCHOR_POINT anchor,
int dx, int dy)
{
Margins margins = {}; // mặc định không điều chỉnh
...
return margins;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Để thống nhất thuật toán, các định nghĩa macro sau về các hướng (các mặt) được đưa vào:
#define LEFT 0x1
#define LOWER 0x2
#define RIGHT 0x4
#define UPPER 0x8
#define CENTER 0x16
2
3
4
5
Với sự trợ giúp của chúng, các mặt nạ bit (kết hợp) được định nghĩa để mô tả các phần tử của các liệt kê ENUM_BASE_CORNER
và ENUM_ANCHOR_POINT
.
const int corner_flags[] = // cờ cho các phần tử ENUM_BASE_CORNER
{
LEFT | UPPER,
LEFT | LOWER,
RIGHT | LOWER,
RIGHT | UPPER
};
const int anchor_flags[] = // cờ cho các phần tử ENUM_ANCHOR_POINT
{
LEFT | UPPER,
LEFT,
LEFT | LOWER,
LOWER,
RIGHT | LOWER,
RIGHT,
RIGHT | UPPER,
UPPER,
CENTER
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Mỗi mảng, corner_flags
và anchor_flags
, chứa chính xác số lượng phần tử tương ứng với liệt kê của nó.
Tiếp theo là mã chính của hàm. Trước tiên, hãy xử lý tùy chọn đơn giản nhất: điểm neo trung tâm.
if(anchor == ANCHOR_CENTER)
{
margins.nearX = margins.farX = dx / 2;
margins.nearY = margins.farY = dy / 2;
}
else
{
...
}
2
3
4
5
6
7
8
9
Để phân tích các tình huống còn lại, chúng ta sẽ sử dụng các mặt nạ bit từ các mảng trên bằng cách truy cập trực tiếp chúng theo các giá trị nhận được corner
và anchor
.
const int mask = corner_flags[corner] & anchor_flags[anchor];
...
2
Nếu góc và điểm neo ở cùng một phía ngang, điều kiện sau sẽ hoạt động và chiều rộng của đối tượng ở cạnh xa của cửa sổ sẽ được điều chỉnh.
if((mask & (LEFT | RIGHT)) != 0)
{
margins.farX = dx;
}
...
2
3
4
5
Nếu chúng không ở cùng một phía, thì chúng có thể ở các phía đối diện, hoặc có thể là trường hợp điểm neo ở giữa cạnh ngang (trên hoặc dưới). Việc kiểm tra điểm neo ở giữa được thực hiện bằng biểu thức (anchor_flags[anchor] & (LEFT | RIGHT)) == 0
- khi đó điều chỉnh bằng nửa chiều rộng của đối tượng.
else
{
if((anchor_flags[anchor] & (LEFT | RIGHT)) == 0)
{
margins.nearX = dx / 2;
margins.farX = dx / 2;
}
else
{
margins.nearX = dx;
}
}
...
2
3
4
5
6
7
8
9
10
11
12
13
Ngược lại, với hướng đối lập của góc và điểm neo, chúng ta điều chỉnh chiều rộng của đối tượng ở biên gần của cửa sổ.
Các kiểm tra tương tự được thực hiện cho trục Y.
if((mask & (UPPER | LOWER)) != 0)
{
margins.farY = dy;
}
else
{
if((anchor_flags[anchor] & (UPPER | LOWER)) == 0)
{
margins.farY = dy / 2;
margins.nearY = dy / 2;
}
else
{
margins.nearY = dy;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Bây giờ hàm GetMargins
đã sẵn sàng, và chúng ta có thể chuyển sang mã chính của script trong hàm OnStart
. Như trước đây, chúng ta xác định kích thước của cửa sổ, tính toán tọa độ ban đầu ở trung tâm, tạo một đối tượng OBJ_LABEL
, và chọn nó.
void OnStart()
{
const int t = ChartWindowOnDropped();
Comment(EnumToString(Corner));
const string name = "ObjSizeLabel";
int h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, t) - 1;
int w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - 1;
int x = w / 2;
int y = h / 2;
ObjectCreate(0, name, OBJ_LABEL, t, 0, 0);
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, true);
ObjectSetInteger(0, name, OBJPROP_SELECTED, true);
ObjectSetInteger(0, name, OBJPROP_CORNER, Corner);
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Để tạo hiệu ứng động, một vòng lặp vô hạn cung cấp các biến pass
(bộ đếm vòng lặp) và anchor
(điểm neo, sẽ được chọn ngẫu nhiên định kỳ).
int pass = 0;
ENUM_ANCHOR_POINT anchor = 0;
...
2
3
Nhưng có một số thay đổi so với ObjectAnchorLabel.mq5
.
Chúng ta sẽ không tạo ra các chuyển động ngẫu nhiên của đối tượng. Thay vào đó, hãy đặt một tốc độ cố định là 5 pixel theo đường chéo.
int px = 5, py = 5;
Để lưu trữ kích thước của nhãn văn bản, chúng ta sẽ dành sẵn hai biến mới.
int dx = 0, dy = 0;
Kết quả của việc tính toán các khoảng lùi bổ sung sẽ được lưu trong biến m
kiểu Margins
.
Margins m = {};
Tiếp theo là vòng lặp di chuyển và sửa đổi đối tượng. Trong đó, tại mỗi lần lặp thứ 75 (một lần lặp 100 ms, xem thêm), chúng ta chọn ngẫu nhiên một điểm neo mới, tạo một văn bản mới (nội dung của đối tượng) từ nó, và chờ các thay đổi được áp dụng cho đối tượng (gọi ChartRedraw
). Điều này cần thiết vì kích thước của văn bản tự động điều chỉnh theo nội dung, và kích thước mới rất quan trọng để chúng ta tính toán chính xác các khoảng lùi trong lời gọi GetMargins
.
Chúng ta lấy kích thước bằng các lời gọi ObjectGetInteger
với các thuộc tính OBJPROP_XSIZE
và OBJPROP_YSIZE
.
for( ;!IsStopped(); ++pass)
{
if(pass % 75 == 0)
{
// ENUM_ANCHOR_POINT gồm 9 phần tử: chọn ngẫu nhiên một cái
const int r = rand() * 8 / 32768 + 1;
anchor = (ENUM_ANCHOR_POINT)((anchor + r) % 9);
ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor);
ObjectSetString(0, name, OBJPROP_TEXT, " " + EnumToString(anchor)
+ StringFormat("[%3d,%3d] ", x, y));
ChartRedraw();
Sleep(1);
dx = (int)ObjectGetInteger(0, name, OBJPROP_XSIZE);
dy = (int)ObjectGetInteger(0, name, OBJPROP_YSIZE);
m = GetMargins(Corner, anchor, dx, dy);
}
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Khi đã biết điểm neo và tất cả các khoảng cách, chúng ta di chuyển đối tượng. Nếu nó "va" vào tường, chúng ta đổi hướng di chuyển sang ngược lại (px
thành -px
hoặc py
thành -py
, tùy vào phía).
// bật lại từ biên cửa sổ, đối tượng hiển thị hoàn toàn
if(x + px >= w - m.farX)
{
x = w - m.farX + px - 1;
px = -px;
}
else if(x + px < m.nearX)
{
x = m.nearX + px;
px = -px;
}
if(y + py >= h - m.farY)
{
y = h - m.farY + py - 1;
py = -py;
}
else if(y + py < m.nearY)
{
y = m.nearY + py;
py = -py;
}
// tính toán vị trí mới của nhãn
x += px;
y += py;
...
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
Còn lại là cập nhật trạng thái của chính đối tượng: hiển thị tọa độ hiện tại trong nhãn văn bản và gán chúng vào các thuộc tính OBJPROP_XDISTANCE
và OBJPROP_YDISTANCE
.
ObjectSetString(0, name, OBJPROP_TEXT, " " + EnumToString(anchor)
+ StringFormat("[%3d,%3d] ", x, y));
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
...
2
3
4
5
Sau khi thay đổi đối tượng, chúng ta gọi ChartRedraw
và chờ 100ms để đảm bảo hoạt ảnh mượt mà hợp lý.
ChartRedraw();
Sleep(100);
...
2
3
Ở cuối vòng lặp, chúng ta kiểm tra lại kích thước cửa sổ, vì người dùng có thể thay đổi nó trong khi script đang chạy, và chúng ta cũng lặp lại yêu cầu kích thước.
h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, t) - 1;
w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - 1;
dx = (int)ObjectGetInteger(0, name, OBJPROP_XSIZE);
dy = (int)ObjectGetInteger(0, name, OBJPROP_YSIZE);
m = GetMargins(Corner, anchor, dx, dy);
}
2
3
4
5
6
7
Chúng ta đã bỏ qua một số cải tiến khác của script ObjectSizeLabel.mq5
để giữ cho phần giải thích ngắn gọn. Những ai muốn có thể tham khảo mã nguồn. Cụ thể, các màu sắc nổi bật đã được sử dụng cho văn bản: mỗi màu cụ thể tương ứng với điểm neo riêng của nó, giúp các điểm chuyển đổi dễ nhận thấy hơn. Ngoài ra, bạn có thể nhấp Delete
trong khi script đang chạy: điều này sẽ xóa đối tượng đã chọn khỏi biểu đồ và script sẽ tự động kết thúc.