Контейнер візуальних об’єктів, Різне, Програмування, статті

Автор: Юрій Спектор, Королівство Delphi


Проектування об’єктно-орієнтованих систем. Приклад – “Контейнер візуальних об’єктів”.


Розглянемо досить поширену практичну задачу: необхідно реалізувати візуальний контейнер, на якому розміщуються графічні об’єкти. Ці об’єкти повинні бути не тільки намальовані на поверхні цього контейнера, але і доступні користувачеві для різних маніпуляцій, таких як виділення, переміщення з допомогою миші та ін Об’єкти можуть бути різнотипними, отже, вони будуть по-різному виглядати, характеризуватися різними параметрами. Набір операцій, які можна здійснювати по відношенню до них, також буде різним.


Як приклади таких візуальних контейнерів можуть служити полотно векторного графічного редактора, на якому розташовуються фігури-примітиви, всілякі редактори схем, діаграм, засоби для відображення і обробки різних чисельних даних (графіки, діаграми, на яких можна розміщувати допоміжні інструменти) та багато інших.


Малодосвідчені програмісти часто губляться при спробі реалізувати такий візуальний контейнер, і або не знають, з якого боку до нього підступитися, або вибирають нераціональне рішення. В даній статті ми спробуємо спроектувати систему, яка буде вирішувати поставлену задачу. Відразу обмовлюся, тут не буде надано готове рішення на всі випадки життя, яке можна не замислюючись переносити в свій проект. Тут буде лише показаний один з варіантів того, як потрібно діяти для вирішення подібних завдань. А саме, ми спробуємо вирішити цю задачу на прикладі простого редактора блок-схем.



Рисунок 1. Приклад контейнера візуальних об’єктів – редактор блок-схем.

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


Основні поняття.


Отже, визначимося з введеними раніше поняттями, а також доповнимо їх деякими новими. Контейнером будемо називати полотно, на якому об’єкти розміщуються. Як вже було сказано вище, прикладом контейнера можуть служити полотна різних програм-редакторів, наприклад – редактор блок-схем або схем електричних. В якості контейнера можна навіть розглядати поля для деяких ігор, наприклад – шахова дошка.


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


Положення об’єкта на контейнері характеризується координатами. Введемо два поняття – логічні координати і екранні координати. Логічними координатами домовимося називати координати об’єкта або його складових частин (вершини, наприклад) в умовній системі координат контейнера. Наприклад, якщо контейнер являє собою шахову дошку, його логічний розмір буде 8×8 і кожен об’єкт (фігура) буде характеризуватися становищем відповідно до цієї координатної системі. Якщо у нас контейнер – це полотно для редагування блок-схем, то логічний розмір полотна можна задати більш довільно. Наприклад, ми можемо прийняти його рівним 200×100 деяких абстрактних одиниць. А можна взяти розмір відповідно до фізичним розміром аркуша паперу даного формату в міліметрах або дюймах. Наприклад, для листа A4 ми можемо прийняти розмір 210×297 (розмір листа в міліметрах). Таким чином, кожен об’єкт на контейнері буде характеризуватися певними координатами, значення яких не залежать від обраного користувачем масштабу, області перегляду, дозволи екрану і т.д. Навпаки, екранними координатами об’єкта будемо називати його зміщення в пікселях щодо початку координат компонента, що представляє контейнер. Екранні координати нам знадобляться при виведенні об’єкта на канві контейнера, логічними ж ми будемо оперувати у всіх інших випадках, щоб відокремити логіку роботи системи від відображення на екрані. Подібна архітектура, де логіка відокремлена від подання на екрані, носить назву “патерн Модель-вид”. Дуже часто фігурує ще й третє поняття – “контролер” – абстрактний шар, що з’єднує між собою логіку і відображення. Така архітектура має назву “Модель-вид-контролер“.


Ставимо завдання.


Наведемо список об’єктів, які нам знадобляться для складання блок-схем, опишемо їх параметри, зовнішній вигляд і дії, які над ними можна здійснювати.



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


Виходить, що положення кожного об’єкта характеризується двома парами координат – верхнім лівим і правим нижнім кутом описаного прямокутника (для всіх, крім ліній) або координатами кінців (для сполучної лінії). Щоб це нам не спрощувало завдання, введемо ще один блок, у якого кількість вершин може бути довільним:


І набір операцій для нього буде дещо відрізнятися. Взявшись за вершини, ми також зможемо переміщати їх незалежно, взявшись за будь-яку лінію – ми зможемо переміщати об’єкт повністю. Подвійний клацання на лінії – Додасть в цьому місці вершину, а подвійне клацання на вершині – видалить її, але тільки в тому випадку, якщо кількість вершин після видалення не стане менше двох. В іншому випадку – нічого видаляти не будемо.


Об’єкти в контейнері необхідно також конструювати в позиції, яка визначається користувачем. Конструювання всіх блоків, крім “ламаної лінії”, полягатиме в послідовному натисканні лівої кнопки миші в двох точках, при цьому між цими натисканнями об’єкт вже буде відображатися і “тягнутися” за мишею. Ламана буде конструюватися подібним чином – кожен наступний клацання миші буде додавати вершину у позиції клацання. Остання вершина буде також “тягнутися” за мишею. Клацання правою кнопкою означатиме закінчення побудови.


Крім координат, всі блоки, крім сполучної і ламаної лінії, зможуть утримувати певний текст. Отже, до цих блокам додадуться ще один параметр. Для простоти, ми не будемо вводити параметр шрифту, але якщо в цьому буде необхідність – реалізувати це не складе труднощів. Також ми для простоти опустимо інші настройки зовнішнього вигляду, такі як колір, заливка та інші.

Отже, підсумуємо все, що ми визначили для об’єктів.

Параметри:

Операції:

Можна було б запровадити і ще деякі специфічні для об’єкта операції, такі як управління кутом нахилу паралелограма або радіусом закруглення блоку “початок / кінець”, проте додати нові операції не складає ніяких труднощів і ми, щоб не захаращувати приклад, не станемо цього робити.


Кожен об’єкт повинен вміти намалювати себе. При цьому не забудемо, що об’єкт нічого не знає про екранних координатах, а оперує тільки логічними. Об’єкт повинен містити універсальний код, що дозволяє вивести себе на поверхню будь-якого компонента, який буде виступати для нього як контейнер.


Тепер поговоримо про параметри контейнера. Перш за все, задамося його логічними розмірами. Нехай це буде 210 логічних одиниць по горизонталі і 297 – по вертикалі. Тобто це розміри вертикально розташованого аркуша формату A4 в міліметрах. За допомогою миші ми зможемо перемістити вершини об’єкта тільки у вузли деякої сітки. Для простоти, приймемо цей крок рівний одній логічній одиниці. Крок сітки можна було б зробити і довільним, що задається користувачем, а можна було б і не робити сітку зовсім, проте її наявність спростить вирівнювання об’єктів на контейнері. Об’єкти нічого не знатимуть про сітку, їх координати просто будуть задаватися в речових числах, які представляють собою логічні координати. Крок сітки буде враховуватися тільки при переході від екранних координат до логічних – при переводі просто будемо округляти результат з урахуванням цього кроку.


Контейнер також буде дозволяти проводити над об’єктами деякі операції, але ці операції швидше будуть мати відношення до самого контейнера, ніж до об’єктів:



Крім того, у контейнера можна задати масштаб. При масштабі 100% однієї логічної одиниці буде відповідати один міліметр.


Як підійти до вирішення?


Для вирішення будь-якої скільки-небудь складної задачі, її потрібно розбити на більш прості частини. Потім ці частини можна розбити на ще більш дрібні частини і т.д. Правильно розбити задачу на підзадачі – дуже важливий етап, не зробивши цього, ефективне рішення знайти вкрай проблематично. Саме тому, перш ніж сідати за клавіатуру, дуже корисно провести деякий час з олівцем і папером.


Якщо звалити все на купу, то реалізація нашого контейнера буде представляти собою такого собі неповороткого монстра, що містить величезну кількість коду, розібратися в якому навіть самому програмісту, який написав його, часом не так то просто. При цьому якщо в майбутньому знадобиться розширювати систему, додаючи нові об’єкти або операції над ними, весь цей код доведеться переглядати і “перелопачувати”. Не саме приємне заняття, загрожує занесенням купи помилок. Рано чи пізно цей “монстр” звалиться під своїм власним вагою. Так що ні про яку гнучкості і масштабованості побудованої таким чином системи говорити не доводиться.


Для ефективного вирішення, доцільно розбити систему на якісь абстрактні блоки, що реалізують строго свою частину завдання і певним чином взаємодіють між собою. В даному випадку, розумно було б спроектувати контейнер таким чином, щоб він міг одноманітно працювати з будь-якими об’єктами, не замислюючись про їх конкретному типі. А всю специфіку поведінки конкретних об’єктів перекласти на самі ці об’єкти. Таким чином, код контейнера стане поліморфним – тобто узагальнений код, який оперує якимись абстрактними поняттями, конкретне поведінка якого залежить від конкретних типів об’єктів, для якого він виконується. Написаний одного разу, такий код буде працювати з будь-яким типом об’єктів, головне щоб ці об’єкти виконували деякі вимоги до них.

Отже, реалізацію поставленої задачі можна розбити на такі частини:

Подібним розбивкою, ми відокремлюємо логіку від виду, що дозволяє нам реалізувати кожну частину незалежно, не змішуючи все до купи. До речі, спроектовану таким чином модель можна віднести до типу “Клієнт-сервер”. Однією з особливостей і переваг такої моделі є саме те, що клієнта аніскільки не хвилює особливості реалізації сервера і навпаки. Головне, щоб між клієнтом і сервером був узгоджений інтерфейс (або протокол) взаємодії. Об’єкти будуть виступати свого роду сервером, який приймає і обробляє запити клієнта – контейнера. Взаємодія здійснюється через строго певний інтерфейс. Контейнер ж буде просто зберігати список об’єктів, керувати ним, а також в залежності від необхідного дії – відправляти їм різні команди-запити. Саме контейнер буде виступати своєрідним посередником між об’єктами і користувачем – приймати від користувача команди, перенаправляти їх об’єктів, і відображати результат. Таким чином, будучи клієнтом для об’єктів, контейнер одночасно є як би сервером для користувача запитів.


Базовий клас об’єктів.

Ну що ж, завдання ми поставили, з основними етапами її реалізації визначилися, тепер можемо рухатися далі. Опишемо базовий клас для всіх об’єктів, в який включимо:
Тепер по порядку реалізуємо це в базовому класі.

Управління позицією об’єкта.


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


І базові точки, і вершини об’єкта зберігають координати в логічних одиницях, а конкретна точка або вершина ідентифікується за індексом. Внутрішньо, об’єкт буде зберігати свою позицію у вигляді списку базових точок, так як цього цілком достатньо для однозначного визначення позиції. Але ззовні базові точки будуть недоступні. Замість цього об’єкт буде надавати доступ до своїх вершин. Список вершин в об’єкті спеціально зберігати немає сенсу, достатньо “навчити” об’єкт повертати координати зазначеної вершини, маючи на руках тільки координати базових точок.

Для завдання логічних координат точок введемо тип TFloatPoint.
type / / Логіка координати
PFloatPoint = ^TFloatPoint;
TFloatPoint = record
X, Y: Extended;
end;

Список базових точок реалізуємо за допомогою класу TList, в якому будемо зберігати покажчики на координати. Пам’ять для цих координат будемо виділяти і звільняти динамічно.

/ / Базовий клас візуальних об’єктів
TBaseVisualObject = class(TObject)
private
FBasePoints: TList;
FOnChange: TNotifyEvent;
FLockCount: Integer;
function GetBasePointsCount: Integer;
function GetBasePoint(Index: Integer): TFloatPoint;
procedure SetBasePoint(Index: Integer; const Value: TFloatPoint);
protected
procedure Change; / / Методи управління базовими точками. Тільки для використання в нащадках, / / Клієнтського коду вони не доступні
procedure AddBasePoint(X, Y: Extended);
procedure InsertBasePoint(Index: Integer; X, Y: Extended);
procedure DeleteBasePoint(Index: Integer);
procedure ClearBasePoints;
property BasePointsCount: Integer read GetBasePointsCount;
property BasePoints[Index: Integer]: TFloatPoint read GetBasePoint
write SetBasePoint; / / Методи управління вершинами. Відповідність між вершинами і базовими точками / / Буде задано в нащадках
function GetVertexesCount: Integer; virtual; abstract;
function GetVertex(Index: Integer): TFloatPoint; virtual; abstract;
procedure SetVertex(Index: Integer; const Value: TFloatPoint); virtual; abstract;
public
constructor Create;
destructor Destroy; override; / / Методи блокування / розблокування
procedure BeginUpdate;
procedure EndUpdate; / / Властивості / події
property VertexesCount: Integer read GetVertexesCount;
property Vertex[Index: Integer]: TFloatPoint read GetVertex write SetVertex;
property OnChange: TNotifyEvent read FOnChange write FOnChange;
end;

implementation
{ TBaseVisualObject }
procedure TBaseVisualObject.AddBasePoint(X, Y: Extended);
var
NewBasePoint: PFloatPoint;
begin / / Виділяємо пам’ять під нову точку і додаємо покажчик на неї в список
New(NewBasePoint);
NewBasePoint^.X := X;
NewBasePoint^.Y := Y;
FBasePoints.Add(NewBasePoint);
Change;
end;
procedure TBaseVisualObject.BeginUpdate;
begin
Inc(FLockCount);
end;
procedure TBaseVisualObject.Change;
begin
if Assigned(FOnChange) and (FLockCount = 0) then
FOnChange(Self);
end;
procedure TBaseVisualObject.ClearBasePoints;
var
i: Integer;
begin / / Звільняємо пам’ять під базові точки і очищаємо список від покажчиків на них
for i := 0 to FBasePoints.Count – 1 do
Dispose(PFloatPoint(FBasePoints[i]));
FBasePoints.Clear;
Change;
end;
constructor TBaseVisualObject.Create;
begin
inherited Create;
FBasePoints := TList.Create;
end;
procedure TBaseVisualObject.DeleteBasePoint(Index: Integer);
begin / / Звільняємо пам’ять, виділену для зберігання координат базових крапок, і видаляємо / / Покажчик зі списку
Dispose(PFloatPoint(FBasePoints[Index]));
FBasePoints.Delete(Index);
Change;
end;
destructor TBaseVisualObject.Destroy;
var
i: Integer;
begin / / Перед знищенням списку, звільняємо пам’ять під вершини
for i := 0 to FBasePoints.Count – 1 do
Dispose(PFloatPoint(FBasePoints[i]));
FBasePoints.Free;
inherited Destroy;
end;
procedure TBaseVisualObject.EndUpdate;
begin
FLockCount := Max(0, FLockCount – 1);
if FLockCount = 0 then
Change;
end;
function TBaseVisualObject.GetBasePoint(Index: Integer): TFloatPoint;
begin
Result := PFloatPoint(FBasePoints[Index])^;
end;
function TBaseVisualObject.GetBasePointsCount: Integer;
begin
Result := FBasePoints.Count;
end;
procedure TBaseVisualObject.InsertBasePoint(Index: Integer; X, Y: Extended);
var
NewBasePoint: PFloatPoint;
begin / / Виділяємо пам’ять під нову точку і додаємо покажчик на неї в список
New(NewBasePoint);
NewBasePoint^.X := X;
NewBasePoint^.Y := Y;
FBasePoints.Insert(Index, NewBasePoint);
Change;
end;
procedure TBaseVisualObject.SetBasePoint(Index: Integer;
const Value: TFloatPoint);
begin
PFloatPoint(FBasePoints[Index])^ := Value;
Change;
end;

Як вже було сказано вище, управління базовими точками клієнтського коду недоступно. Базові точки – це “внутрішня кухня” об’єктів, ми будемо використовувати їх при реалізації конкретних нащадків класу, наприклад, створюючи об’єкт прямокутної форми, в конструкторі класу ми відразу ж додамо дві базові точки, які і будуть керувати позицією об’єкта. В об’єкті “ламана лінія” ми зможемо додавати і видаляти точки довільно. До речі, в об’єкті “лінія” і “ламана лінія” базові точки будуть повністю еквівалентні вершин.


Управління вершинами здійснюється через властивість Vertex [Index], причому методи отримання / встановлення властивості створені віртуальними і абстрактними. В залежності від типу об’єкта, ми напишемо відповідну реалізацію цих методів, які будуть зв’язувати між собою вершини з базовими точками.


Також варто звернути увагу на метод Change. Цей метод викликається при будь-якій зміні стану об’єкта. В ньому перевіряється, призначений чи обробник події OnChange, і якщо так, і при цьому не було блокувань – То викликає цей обробник. Його можна використовувати для того, щоб клієнтський код міг відреагувати на зміну об’єктів (наприклад, викликати перемальовування об’єктів з урахуванням змінених параметрів). Методи блокувань BeginUpdate і EndUpdate будемо використовувати тоді, коли потрібно тимчасово заборонити викликати оброблювач OnChange, зокрема, коли змін стану об’єкта планується багато, але при цьому немає сенсу щоразу викликати OnChange – достатньо одного дзвінка в кінці. Виклик BeginUpdate збільшує на 1 лічильник блокувань, а EndUpdate – відповідно зменшує, при цьому, не даючи йому прийняти негативне значення – т.зв. “Захист від дурня”. Якщо в результаті зняття блокування лічильник став рівний 0 (всі блокування зняті), то викликається метод Change щоб дозволити зухвалому коду відреагувати на зміни.


Механізми обробки операцій.


Для здійснення операцій над об’єктами, необхідно якимось чином відправити йому команду, яка буде містити код конкретної операції, а також деякий набір параметрів. Об’єкт, отримавши цю команду, відповідним чином на неї відреагує. Дуже нагадує механізм, за яким вікна в Window отримують і обробляє повідомлення. Було б зручно описати методи з директивою message, для обробки кожного конкретного типу операції, а потім просто відправляти об’єкту повідомлення. До того ж, для додавання нових типів команд не довелося б сильно переробляти всю архітектуру, достатньо було б у потрібних класів додати обробник нового повідомлення. Однак наші об’єкти не є вікнами Windows. Як бути?


Відповідь полягає в тому, що в Delphi методи з директивою message можна описувати у будь-яких класів, не обов’язково у віконних елементів управління. Метод Dispatch, який дозволяє знаходити і викликати такі методи, реалізований в класі TObject. Нам залишається тільки додати в клас метод, назвавши його, скажімо, SendCommand, що приймає ідентифікатор і параметри повідомлення, а в реалізації цього методу просто викликати Dispatch. Тепер виклик SendCommand буде призводити до виклику методу, описаного директивою message, відповідного ідентифікатора відправляється. Зробимо це.

TBaseVisualObject = class(TObject)

public
… / / Відправка команди
function SendCommand(ID: Cardinal; wParam, lParam: Longint): Longint;

end;
implementation

function TBaseVisualObject.SendCommand(ID: Cardinal; wParam,
lParam: Longint): Longint;
var
Message: TMessage;
begin
Message.Msg := ID;
Message.WParam := wParam;
Message.LParam := lParam;
Dispatch(Message);
Result := Message.Result;
end;

Деякі операції та їх обробку можна визначити ще в базовому класі. В нащадках, при необхідності, ми завжди зможемо перекрити цю поведінку своїх. У базовому ж класі визначимо наступні операції:



При отриманні першої команди зі списку будемо переводити об’єкт у відповідний режим та зберігати стартові параметри. При отриманні другої команди – виводити об’єкт з цього режиму. Сенс третьої операції буде зводитися до того, щоб визначити, за яку частину об’єкта тягне користувач (вершину, сторону, внутрішню область) і відповідно оправляти більш конкретну команду з трьох останніх у списку.


Перш, ніж оголосити типи повідомлень і написати реалізацію їх обробників, визначимося, як ми будемо відрізняти конкретні області об’єктів, такі як вершини, сторони, внутрішня область. Для ідентифікації області будемо використовувати тип Cardinal – 4-хбайтное беззнаковое ціле. Окремі біти цього числа будуть кодувати тип області (вершина, сторона і т.д.) і її індекс.

const / / Області об’єктів HT_OUT = $ 00000000; / / Поза об’єкта HT_IN = $ 80000000; / / Внутрішня область HT_VERTEX = $ 40000000; / / Вершина HT_SIDE = $ 20000000; / / Сторона

Індекс вершини і сторони буде додаватися відповідно до константам HT_VERTEX і HT_SIDE. Тепер визначимося з типами і параметрами повідомлень.

const
… / / Команди об’єктів
VOC_BEGINDRAG = 1;
VOC_ENDDRAG = 2;
VOC_DRAG = 3;
VOC_VERTEXMOVE = 4;
VOC_SIDEMOVE = 5;
VOC_MOVE = 6;
type
… / / Початок перетягування або розтягування мишею
TVOCBeginDrag = packed record
CmdID: Cardinal; HitTest: Cardinal; / / Область об’єкта StartPos: PFloatPoint; / / Позиція, в якій почалося перетягування
Result: Longint;
end;
/ / Завершення перетягування або розтягування мишею
TVOCEndDrag = packed record
CmdID: Cardinal; Unused1: Longint; / / Не використовується Unused2: Longint; / / Не використовується
Result: Longint;
end;
/ / Перетягування або розтягування мишею
TVOCDrag = packed record
CmdID: Cardinal; Unused: Longint; / / Не використовується NewPos: PFloatPoint; / / Позиція, в яку перемістилася миша
Result: Longint;
end;
/ / Переміщення вершини
TVOCVertexMove = packed record
CmdID: Cardinal; Index: Integer; / / Індекс вершини NewPos: PFloatPoint; / / Нова позиція вершини
Result: Longint;
end;
/ / Переміщення боку
TVOCSideMove = packed record
CmdID: Cardinal; Index: Integer; / / Індекс боку NewPos: PFloatPoint; / / Нова позиція боку
Result: Longint;
end;
/ / Переміщення вершини
TVOCMove = packed record
CmdID: Cardinal; DeltaX: PExtended; / / Зсув по осі X DeltaY: PExtended; / / Зсув по осі Y
Result: Longint;
end;

Реалізуємо в базовому класі обробники цих команд по замовчуванню. Метод обробки VOC_SIDEMOVE в базовому класі реалізовувати не будемо, поведінка об’єктів при отриманні цієї команди визначимо у нащадків.

type

TBaseVisualObject = class(TObject)
private

FDragging: Boolean;
FDragHitTest: Cardinal;
FDragStartPos: TFloatPoint;

procedure VOCBeginDrag(var Command: TVOCBeginDrag); message VOC_BEGINDRAG;
procedure VOCEndDrag(var Command: TVOCEndDrag); message VOC_ENDDRAG;
procedure VOCDrag(var Command: TVOCDrag); message VOC_DRAG;
procedure VOCVertexMove(var Command: TVOCVertexMove); message VOC_VERTEXMOVE;
procedure VOCMove(var Command: TVOCMove); message VOC_MOVE;

implementation

procedure TBaseVisualObject.VOCBeginDrag(var Command: TVOCBeginDrag);
begin
FDragging := True;
FDragHitTest := Command.HitTest;
FDragStartPos := Command.StartPos^;
end;
procedure TBaseVisualObject.VOCDrag(var Command: TVOCDrag);
var
HitTest: Cardinal;
Index: Integer;
DeltaX, DeltaY: Extended;
begin
if FDragging then begin / / Розкладаємо FDragHitTest на загальний код області та індекс
HitTest := FDragHitTest and $FFFF0000;
Index := FDragHitTest and $0000FFFF; / / В залежності від того, над якою областю миша, посилаємо різні / / Команди
case HitTest of
HT_IN:
begin / / Визначаємо величину зміщення
DeltaX := Command.NewPos^.X – FDragStartPos.X;
DeltaY := Command.NewPos^.Y – FDragStartPos.Y; / / Наступного разу зсув будемо рахувати від поточної позиції
FDragStartPos := Command.NewPos^;
SendCommand(VOC_MOVE, Longint(@DeltaX), Longint(@DeltaY));
end;
HT_VERTEX: SendCommand(VOC_VERTEXMOVE, Index, Longint(Command.NewPos));
HT_SIDE: SendCommand(VOC_SIDEMOVE, Index, Longint(Command.NewPos));
end;
end;
end;
procedure TBaseVisualObject.VOCEndDrag(var Command: TVOCEndDrag);
begin
FDragging := False;
end;
procedure TBaseVisualObject.VOCMove(var Command: TVOCMove);
var
i: Integer;
Pos: TFloatPoint;
begin
BeginUpdate;
try / / Переміщаємо всі базові точки на величину зміщення
for i := 0 to BasePointsCount – 1 do begin
Pos := BasePoints[i];
Pos.X := Pos.X + Command.DeltaX^;
Pos.Y := Pos.Y + Command.DeltaY^;
BasePoints[i] := Pos;
end;
finally
EndUpdate;
end;
end;
procedure TBaseVisualObject.VOCVertexMove(var Command: TVOCVertexMove);
begin
Vertex[Command.Index] := Command.NewPos^;
end;

Промальовування об’єкта.


Кожен об’єкт повинен вміти намалювати себе. Здавалося б, просте завдання – ввести в базовому класі віртуальний метод Draw, який і буде малювати об’єкт на переданої йому як параметр канві TCanvas. Але не все так просто. Об’єкт знає про свою позицію, але тільки в логічних одиницях, які для TCanvas не мають ніякого сенсу. Звичайно, можна в цей метод передавати ще й необхідні коефіцієнти для перекладу з однієї системи координат в інші, але ми зробимо інакше.


Реалізуємо клас TLogicalCanvas, який буде представляти собою щось на зразок моста з логічної системи координат в екранні. Об’єкт буде малювати себе, викликаючи методи TLogicalCanvas, які в якості параметрів приймають логічні координати, а вже всередині цих методів буде йти трансляція і промальовування на TCanvas. Це додасть нашій системі додаткову гнучкість. Наприклад, ми можемо в класі TLogicalCanvas реалізувати методи малювання віртуальними, оголосити у класу нащадки і там їх перекрити. Наприклад, в одному нащадку будуть малюватися прості лінії і фігури на двовимірному полотні. В іншому нащадку – вони можуть бути зображені, наприклад, в тривимірному просторі. Саме для редактора блок-схем, така поведінка, зрозуміло, не потрібно, але під поняття “контейнер візуальних об’єктів” потрапляє досить багато завдань, а в деяких з них це може знадобитися. Зате в нашому випадку, ми зможемо не перекладати на об’єкт повністю відповідальність за свій вигляд, а лише сказати йому, з яких складових частин він складається. Самі ці складові частини можуть виглядати інакше, і ми зможемо гнучко цим керувати, наприклад, нам не важко буде в одному випадку позначати в об’єкта вершини квадратами, в іншому – колами, а в третьому – і зовсім не позначати, а будувати об’єкт тільки з ліній. При додаванні нового способу відображення, змінювати код самого об’єкта не доведеться. Знову модель і вид відокремлені один від одного. Проте реалізовувати нащадки TLogicalCanvas ми не станемо, а даний клас введемо чисто для демонстрації описаного вище підходу.


Як вже було сказано вище, клас TLogicalCanvas повинен вміти переходити з однієї системи координат в іншу. Однак закладати цю можливість в сам клас сенсу мало. Справа в тому, що переклад з однієї системи координат в іншу може знадобиться не тільки для малювання об’єктів, а й, скажімо, при визначенні області об’єкта під курсором миші. Очевидно, що код перетворення координат в цих випадках повинен бути ідентичним і його немає сенсу дублювати. Виходить, що TLogicalCanvas повинен звертатися до будь-якого зовнішнього об’єкта (імовірно – контейнеру) для того, щоб викликати його методи перетворення координат. Не зовсім гарне рішення – якийсь абстрактний клас, що представляє собою полотно, повинен щось знати про конкретному класі, який такі перетворення може здійснювати. До того ж, нам не потрібна вся функціональність цього конкретного класу, а тільки лише методи перетворення координат.


Тут слід згадати про одну можливості мови, яка в даному випадку може стати в нагоді – інтерфейси. Інтерфейс являє собою простий опис методів, реалізацію яких має взяти на себе клас. Коли методи інтерфейсу в класі реалізовані (не важливо в якому), ми можемо привести екземпляр цього класу до интерфейсному типу і передати отриману інтерфейсну посилання туди, де ці методи можуть знадобитися. Знаючи тільки опис інтерфейсу, можна викликати його методи, при цьому, абсолютно не замислюючись про інші крім самого інтерфейсу сутності. Дуже зручно виходить – клас TLogicalCanvas не знає, хто саме і яким чином реалізує методи перетворення координат, але йому надана можливість ці методи викликати.


Оголосимо інтерфейс, в якому визначимо методи перекладу з логічних одиниць в екранні і навпаки.

type / / Інтерфейс для перетворення координат
ICoordConvert = interface(IInterface)
procedure LogToScreen(lX, lY: Extended; var sX, sY: Integer); overload;
procedure ScreenToLog(sX, sY: Integer; var lX, lY: Extended); overload;
function LogToScreen(Value: Extended): Integer; overload;
function ScreenToLog(Value: Integer): Extended; overload;
end;

Тут варто пояснити. Ми визначили дві пари методів з однаковими назвами, але різним набором параметрів. Перша пара методів приймає в якості параметрів координати точки в одній системі і повертає в іншій. Будучи реалізовані в контейнері, ці методи будуть сильно залежати від його стану – вибраного масштабу, видимої в компоненті області, яка задається положенням скроллбаров. Друга ж пара методів, по суті, залежить тільки від вибраного масштабу. Ці методи будуть повертати не абсолютні координати точки, а просто перекладати (у нашому випадку) пікселі в міліметри і навпаки. Нам знадобляться обидві пари методів.


При створенні екземпляра TLogicalCanvas будемо передавати йому параметр типу ICoordConvert, за допомогою якого і здійснюватимуться координатні перетворення.


Для прикладу покажемо, як у цьому класі буде реалізований метод, який малює відрізок. Реалізація малювання інших примітивів принципово нічим не відрізняється.

type / / Логічний полотно
TLogicalCanvas = class(TObject)
private
FCanvas: TCanvas;
FConvertIntf: ICoordConvert;
public
constructor Create(Canvas: TCanvas; ConvertIntf: ICoordConvert); / / Методи малювання
procedure DrawLine(X1, Y1, X2, Y2, LineWidth: Extended); … {Будуть і інші методи для малювання}
end;
implementation
{ TLogicalCanvas }
constructor TLogicalCanvas.Create(Canvas: TCanvas;
ConvertIntf: ICoordConvert);
begin
inherited Create;
FCanvas := Canvas;
FConvertIntf := ConvertIntf;
end;
procedure TLogicalCanvas.DrawLine(X1, Y1, X2, Y2, LineWidth: Extended);
var
sX1, sY1, sX2, sY2: Integer;
begin / / Перехід в екранні координати
FConvertIntf.LogToScreen(X1, Y1, sX1, sY1);
FConvertIntf.LogToScreen(X2, Y2, sX2, sY2); / / Ширина лінії
FCanvas.Pen.Width := FConvertIntf.LogToScreen(LineWidth); / / Малюємо
FCanvas.MoveTo(sX1, sY1);
FCanvas.LineTo(sX2, sY2);
end;

Тут все досить просто і очевидно, в докладних коментарях немає потреби. В методи малювання передаються параметри в логічних одиницях, в цих методах здійснюється перехід в екранні координати і висновок на канву, яку ми вказали в конструкторі.


Тепер оголосимо у базового класу об’єктів віртуальний метод Draw, який приймає логічну канву як параметр. В нащадках цей метод ми перекриємо, а в базовому класі визначимо загальну реалізацію – малювання всіх вершин.

TBaseVisualObject = class(TObject)

public
… / / Малювання
procedure Draw(Canvas: TLogicalCanvas); virtual;

implementation

procedure TBaseVisualObject.Draw(Canvas: TLogicalCanvas);
var
i: Integer;
begin / / Малюємо вершини
for i := 0 to VertexesCount – 1 do
Canvas.DrawVertex(Vertex[i].X, Vertex[i].Y);
end;

Визначення області об’єкта в точці.


Добре, а як клієнтський код визначить, яка область об’єкта знаходиться під покажчиком миші? Об’єкт повинен мати метод, який приймає координати в якості параметрів, і повертає код області. Причому потрібно врахувати, що користувачеві досить проблематично підвести курсор миші прямо на вершину, і тому деякі її околиці також повинні сприйматися, як вершина. Те ж стосується і сторін. І знову, необхідно врахувати, що координати миші задаються в екранних одиницях, а об’єкт знає своє положення тільки у фізичних координатах. Значить, для цього дії нам знову доведеться використовувати раніше введений інтерфейс.


Дану операцію також можна реалізувати в якості команди. Її обробник в базовому класі буде просто повертати HT_OUT, а вже в конкретних нащадках ми перекриємо цю операцію, щоб об’єкт правильно повідомляв свої області в точках.


Крім того, при діях над об’єктом за допомогою миші, в залежності від того, над якою його частиною знаходяться миша, необхідно відповідним чином змінювати вид курсора. Можна ввести ще одну команду, в яку передається код області в якості параметра, а результатом повертається ідентифікатор курсора. Для ідентифікації курсора в Delphi є спеціальний тип – TCursor. Однак ми не будемо однозначно “зашивати” в об’єкти вид курсора, логічно було б залишити цю можливість використовує об’єкт контейнеру. В об’єкті ми будемо формувати свій незалежний код курсора, а вже контейнер нехай робить з ним що захоче. Наприклад, приймає різну форму курсора для різних кодів, або один загальний вид курсора (скажімо, у формі руки) для всіх.

const
… / / Види курсора
CR_DEFAULT = 0;
CR_SIZEALL = 1;
CR_HORIZONTAL = 2;
CR_VERTICAL = 3;
CR_DIAG1 = 4;
CR_DIAG2 = 5;

VOC_HITTEST = 7;
VOC_GETCURSOR = 8;
type / / Параметри для визначення області об’єкта
PHitTestParams = ^THitTestParams;
THitTestParams = record XPos, YPos: Integer; / / Позиція в екранних одиницях Tolerance: Integer; / / Чутливість
end;
… / / Визначення області об’єкта
TVOCHitTest = packed record
CmdID: Cardinal; ConvertIntf: ICoordConvert; / / Інтерфейс перетворення координат Params: PHitTestParams; / / Параметри
Result: Cardinal;
end;
/ / Визначення виду курсора
TVOCGetCursor = packed record
CmdID: Cardinal; Unused: Longint; / / Не використовується HitTest: Cardinal; / / Область об’єкта
Result: Cardinal;
end;
/ / Базовий клас візуальних об’єктів
TBaseVisualObject = class(TObject)
private

procedure VOCHitTest(var Command: TVOCHitTest); message VOC_HITTEST;

implementation

procedure TBaseVisualObject.VOCGetCursor(var Command: TVOCGetCursor);
begin
Command.Result := CR_DEFAULT;
end;
procedure TBaseVisualObject.VOCHitTest(var Command: TVOCHitTest);
begin
Command.Result := HT_OUT;
end;

Конструювання об’єкта користувачем.

Для конструювання об’єктів також має сенс визначити кілька нових команд:

VOC_CONSTRUCTPOINT
– Будемо викликати при клацанні лівою кнопкою миші в режимі конструювання. Обробка цієї команди буде фіксувати чергову точку об’єкта. Причому, якщо результатом команда поверне нуль, це буде ознакою того, що конструювання об’єкта закінчено.
VOC_PROCESSCONSTRUCT
– Цю команду будемо відправляти в режимі конструювання при переміщенні миші між натисканнями. Вона потрібна для того, щоб оновити позицію об’єкта з урахуванням поточного положення курсора.
VOC_STOPCONSTRUCT
– Будемо викликати при натисканні правої кнопки миші в режимі конструювання для того, щоб примусово його завершити. При цьому якщо результатом команда поверне нуль, це буде означати, що конструювання завершено занадто рано, позиція об’єкта не задана і його слід знищити. Ця можливість допоможе нам скасовувати створення будь-якого об’єкта після натискання правою кнопкою миші. Для ламаної повністю скасувати конструювання можна буде тільки після установки першої вершини, клацання правою кнопкою після установки наступних вершин просто завершить конструювання.
const

VOC_PROCESSCONSTRUCT = 10;
VOC_STOPCONSTRUCT = 11;

type
… / / Додавання точки при конструюванні
TVOCConstructPoint = packed record
CmdID: Cardinal; Unused: Longint; / / Не використовується Pos: PFloatPoint; / / Позиція нової точки
Result: Longint;
end;
/ / Конструювання
TVOCProcessConstruct = packed record
CmdID: Cardinal; Unused: Longint; / / Не використовується Pos: PFloatPoint; / / Позиція
Result: Longint;
end;
/ / Завершення конструювання
TVOCStopConstruct = packed record
CmdID: Cardinal; Unused1: Longint; / / Не використовується Unused2: Longint; / / Не використовується
Result: Longint;
end;

Процес конструювання буде дещо відрізнятися для різних типів об’єктів, тому з реалізацією обробників цих команд – почекаємо.


Що далі?


Схоже, що з базовим класом ми повністю визначилися. Прийшов час зайнятися його нащадками. Для початку згадаємо, які об’єкти ми вирішили реалізувати. Можна помітити, що ці об’єкти можна розділити на дві групи:



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


Прямокутні об’єкти.


Перш ніж приступити до реалізації базового для “прямокутних” об’єктів класу, подумаємо, що в нього можна включити.



Тобто по суті, нащадкам даного класу залишиться тільки перевизначити власну реалізацію методу Draw.

Задамо відповідність між вершинами і базовими точками.
type / / Базовий клас для “прямокутних” об’єктів
TRectVisualObject = class(TBaseVisualObject)
protected
function GetVertexesCount: Integer; override;
function GetVertex(Index: Integer): TFloatPoint; override;
procedure SetVertex(Index: Integer; const Value: TFloatPoint); override;
public
constructor Create;
end;

implementation

{ TRectVisualObject }
constructor TRectVisualObject.Create;
begin
inherited Create;
AddBasePoint(0, 0);
AddBasePoint(0, 0);
end;
function TRectVisualObject.GetVertex(Index: Integer): TFloatPoint;
begin / / 0 – лівий верхній кут / / 1 – правий верхній кут / / 2 – правий нижній кут / / 3 – лівий нижній кут
case Index of
0: Result := BasePoints[0];
1:
begin
Result.X := BasePoints[1].X;
Result.Y := BasePoints[0].Y;
end;
2: Result := BasePoints[1];
3:
begin
Result.X := BasePoints[0].X;
Result.Y := BasePoints[1].Y;
end;
else
TList.Error(@SListIndexError, Index);
end;
end;
function TRectVisualObject.GetVertexesCount: Integer;
begin
Result := 4;
end;
procedure TRectVisualObject.SetVertex(Index: Integer;
const Value: TFloatPoint);
var
Point: TFloatPoint;
begin / / Встановлюємо нові значення базовим точкам з урахуванням того, що 0-а точка / / Завжди повинна бути лівіше і вище першого
case Index of
0:
begin
Point := BasePoints[0];
Point.X := Min(Value.X, BasePoints[1].X);
Point.Y := Min(Value.Y, BasePoints[1].Y);
BasePoints[0] := Point;
end;
1:
begin
Point := BasePoints[1];
Point.X := Max(Value.X, BasePoints[0].X);
BasePoints[1] := Point;
Point := BasePoints[0];
Point.Y := Min(Value.Y, BasePoints[1].Y);
BasePoints[0] := Point;
end;
2:
begin
Point := BasePoints[1];
Point.X := Max(Value.X, BasePoints[0].X);
Point.Y := Max(Value.Y, BasePoints[0].Y);
BasePoints[1] := Point;
end;
3:
begin
Point := BasePoints[0];
Point.X := Min(Value.X, BasePoints[1].X);
BasePoints[0] := Point;
Point := BasePoints[1];
Point.Y := Max(Value.Y, BasePoints[0].Y);
BasePoints[1] := Point;
end;
else
TList.Error(@SListIndexError, Index);
end;
end;

У конструкторі відразу ж додаються дві базові точки в координатах (0; 0). Можна було б позиціонувати об’єкт спочатку по-іншому, але не суть. Головне те, що більше кількість базових точок ніде змінюватися не буде. А ось їх положення можна змінити, переміщаючи вершини або сторони об’єкта. Коли ми змінюємо положення вершини, викликається метод SetVertex, який викликає відповідну зміну позицій базових точок. Якщо при зміні положення 0-й або 2-й вершини нам потрібно просто перемістити відповідно 0-ю та 1-ю базову точку, то з 1-ї і 3-й вершиною ситуація дещо складніша. В останньому випадку нам потрібно поміняти по одній координаті у двох базових точок. Метод GetVertex, відповідно, нічого не змінює, а просто повертає позицію вершини з заданим індексом, вираховуючи її з позиції базових точок.

Визначимо дії по розтягуванню об’єкта за його сторону:
type / / Базовий клас для “прямокутних” об’єктів
TRectVisualObject = class(TBaseVisualObject)
private
procedure VOCSideMove(var Command: TVOCSideMove); message VOC_SIDEMOVE;

end;

implementation

{ TRectVisualObject }

procedure TRectVisualObject.VOCSideMove(var Command: TVOCSideMove);
var
Point: TFloatPoint;
begin / / 0 – ліва сторона / / 1 – верхня сторона / / 2 – права сторона / / 3 – нижня сторона
case Command.Index of
0:
begin
Point := Vertex[0];
Point.X := Command.NewPos^.X;
Vertex[0] := Point;
end;
1:
begin
Point := Vertex[0];
Point.Y := Command.NewPos^.Y;
Vertex[0] := Point;
end;
2:
begin
Point := Vertex[2];
Point.X := Command.NewPos^.X;
Vertex[2] := Point;
end;
3:
begin
Point := Vertex[2];
Point.Y := Command.NewPos^.Y;
Vertex[2] := Point;
end;
else
TList.Error(@SListIndexError, Command.Index);
end;
end;

Переміщення сторони (відбувається при отриманні команди VOC_SIDEMOVE) реалізовано таким чином, що ми просто в залежності від індексу боку переміщаємо по одній осі одну з вершин. Наприклад, при переміщенні лівого боку ми просто міняємо значення по осі X лівої верхньої вершини. З таким же успіхом можна було б переміщати по осі X і ліву нижню вершину.


Тепер навчимо об’єкти відгукуватися на запит області під курсором і форми курсора в області:

type / / Базовий клас для “прямокутних” об’єктів
TRectVisualObject = class(TBaseVisualObject)
private

procedure VOCHitTest(var Command: TVOCHitTest); message VOC_HITTEST;
procedure VOCGetCursor(var Command: TVOCGetCursor); message VOC_GETCURSOR;

end;

implementation

{ TRectVisualObject }

procedure TRectVisualObject.VOCGetCursor(var Command: TVOCGetCursor);
begin
case Command.HitTest of
HT_IN: Command.Result := CR_SIZEALL;
HT_VERTEX + 0, HT_VERTEX + 2: Command.Result := CR_DIAG1;
HT_VERTEX + 1, HT_VERTEX + 3: Command.Result := CR_DIAG2;
HT_SIDE + 0, HT_SIDE + 2: Command.Result := CR_HORIZONTAL;
HT_SIDE + 1, HT_SIDE + 3: Command.Result := CR_VERTICAL;
else
Command.Result := CR_DEFAULT;
end;
end;
procedure TRectVisualObject.VOCHitTest(var Command: TVOCHitTest);
var
sX1, sY1, sX2, sY2: Integer;
begin / / Переводимо в екранні координати
Command.ConvertIntf.LogToScreen(BasePoints[0].X, BasePoints[0].Y, sX1, sY1);
Command.ConvertIntf.LogToScreen(BasePoints[1].X, BasePoints[1].Y, sX2, sY2); / / Виявляємо область в точці
Command.Result := HT_OUT;
if (Abs(Command.Params.XPos – sX1) <= Command.Params.Tolerance) and
(Abs(Command.Params.YPos – sY1) <= Command.Params.Tolerance)
then begin / / Вершина 0
Command.Result := HT_VERTEX + 0;
Exit;
end;
if (Abs(Command.Params.XPos – sX2) <= Command.Params.Tolerance) and
(Abs(Command.Params.YPos – sY1) <= Command.Params.Tolerance)
then begin / / Вершина 1
Command.Result := HT_VERTEX + 1;
Exit;
end;
if (Abs(Command.Params.XPos – sX2) <= Command.Params.Tolerance) and
(Abs(Command.Params.YPos – sY2) <= Command.Params.Tolerance)
then begin / / Вершина 2
Command.Result := HT_VERTEX + 2;
Exit;
end;
if (Abs(Command.Params.XPos – sX1) <= Command.Params.Tolerance) and
(Abs(Command.Params.YPos – sY2) <= Command.Params.Tolerance)
then begin / / Вершина 3
Command.Result := HT_VERTEX + 3;
Exit;
end;
if (Abs(Command.Params.XPos – sX1) <= Command.Params.Tolerance) and
(Command.Params.YPos > sY1) and (Command.Params.YPos < sY2)
then begin / / Сторона 0
Command.Result := HT_SIDE + 0;
Exit;
end;
if (Abs(Command.Params.YPos – sY1) <= Command.Params.Tolerance) and
(Command.Params.XPos > sX1) and (Command.Params.XPos < sX2)
then begin / / Сторона 1
Command.Result := HT_SIDE + 1;
Exit;
end;
if (Abs(Command.Params.XPos – sX2) <= Command.Params.Tolerance) and
(Command.Params.YPos > sY1) and (Command.Params.YPos < sY2)
then begin / / Сторона 2
Command.Result := HT_SIDE + 2;
Exit;
end;
if (Abs(Command.Params.YPos – sY2) <= Command.Params.Tolerance) and
(Command.Params.XPos > sX1) and (Command.Params.XPos < sX2)
then begin / / Сторона 1
Command.Result := HT_SIDE + 3;
Exit;
end;
if (Command.Params.XPos > sX1) and (Command.Params.XPos < sX2) and
(Command.Params.YPos > sY1) and (Command.Params.YPos < sY2)
then begin / / Всередині
Command.Result := HT_IN;
Exit;
end;
end;

Метод обробки команди VOC_HITTEST виглядає громіздким, проте і в ньому все досить тривіально. Його можна було б переписати більш компактно, але в такому вигляді він зрозуміліше. Спочатку всі базові точки переводяться в екранні координати, а потім послідовно перевіряємо, чи лежить точка з координатами (Command.Params.XPos; Command.Params.YPos) на одній з вершин / сторін об’єкта (з урахуванням допустимої похибки Command.Params.Tolerance) або всередині його. Наприклад, якщо ми при реалізації контейнера задамо Tolerance рівним 2, то користувач зможе “промахнутися” повз вершини або сторони об’єкта на 2 пікселя, і при цьому буде вважатися, що миша над відповідною частиною об’єкта.


Метод обробки команди VOC_GETCURSOR приймає як параметр область об’єкта і повертає рекомендований вид курсора. Наприклад, для лівого і правого боку він поверне код курсора, який можна інтерпретувати як “горизонтальна двонаправлена ​​стрілка”.


Тепер розберемося з конструюванням таких об’єктів. Воно буде зводитися до двох послідовним клацанням лівою кнопкою миші, між якими, в процесі руху миші, буде відображатися попередній вигляд об’єкта. Якщо після першого клацання лівою кнопкою миші, користувач натисне праву – конструювання об’єкта буде скасовано. Після другого клацання лівою кнопкою – конструювання об’єкта буде закінчено. Реалізуємо все вищесказане за допомогою команд, введених нами раніше.

type / / Базовий клас для “прямокутних” об’єктів
TRectVisualObject = class(TBaseVisualObject)
private

FConstructing: Boolean;
FCurrentPoint: Integer;
procedure VOCConstructPoint(var Command: TVOCConstructPoint);
message VOC_CONSTRUCTPOINT;
procedure VOCProcessConstruct(var Command: TVOCProcessConstruct);
message VOC_PROCESSCONSTRUCT;
procedure VOCStopConstruct(var Command: TVOCStopConstruct);
message VOC_STOPCONSTRUCT;
end;

implementation

{ TRectVisualObject }
procedure TRectVisualObject.VOCConstructPoint(
var Command: TVOCConstructPoint);
begin / / Якщо об’єкт не знаходиться в режимі конструювання – переводимо його в цей / / Режим і встановлюємо початковий номер поточної редагованої точки
if not FConstructing then begin
FConstructing := True;
FCurrentPoint := 0;
end; / / В залежності від номера редагованої точки, виконуємо потрібні дії / / Позиціонування
case FCurrentPoint of
0:
begin / / Переміщаємо всі крапки об’єкта в стартову
BasePoints[0] := Command.Pos^;
BasePoints[1] := Command.Pos^; / / Конструювання не закінчено
Command.Result := 1;
end;
1:
begin / / Переміщаємо точку з індексом 1
BasePoints[1] := Command.Pos^; / / Конструювання закінчено
FConstructing := False;
Command.Result := 0;
end;
else
TList.Error(@SListIndexError, FCurrentPoint);
end; / / Інкремент індексу поточної точки
Inc(FCurrentPoint);
end;
procedure TRectVisualObject.VOCProcessConstruct(
var Command: TVOCProcessConstruct);
begin / / Переміщаємо вершину, відповідну поточної точці.
if FConstructing then
case FCurrentPoint of
0: Vertex[0] := Command.Pos^;
1: Vertex[2] := Command.Pos^;
end;
end;
procedure TRectVisualObject.VOCStopConstruct(
var Command: TVOCStopConstruct);
begin
Command.Result := 1;
if FConstructing then begin / / Виходимо з режиму конструювання сигналізуємо викликає код про те, / / Що об’єкт не добудований до кінця
FConstructing := False;
Command.Result := 0;
end;
end;

При отриманні команди VOC_CONSTRUCTPOINT перший раз – будемо переводити об’єкт в режим конструювання, і переміщати всі його точки в стартову. При отриманні другий раз – встановлюємо позицію другою фундаментальною точки і виходимо з режиму конструювання. У параметрі Result повертаємо 0, сигналізує про те, що конструювання закінчено. В обробнику команди VOC_PROCESSCONSTRUCT просто оновлюємо позицію вершини, відповідною поточною редагованої точки. Саме вершину змінюємо, а не базову точку, так як при зміні вершини контролюється, щоб ми другу точку не засунули лівіше або вище першої. В обробнику VOC_STOPCONSTRUCT – виходимо з режиму конструювання і сигналізуємо про те, що конструювання скасовано, і об’єкт слід знищити. Відправляє команду код повідомляється про це через параметр Result, в якому записуємо 0.


Ну і останнє – додамо властивість, що задає текст елемента. Реалізація в коментарів не потребує.

type / / Базовий клас для “прямокутних” об’єктів
TRectVisualObject = class(TBaseVisualObject)
private

FText: String;
procedure SetText(const Value: String);

public / / Текст елемента
property Text: String read FText write SetText;
end;

implementation

{ TRectVisualObject }
procedure TRectVisualObject.SetText(const Value: String);
begin
if FText <> Value then begin
FText := Value;
Change;
end;
end;
З прямокутними об’єктами закінчили, можна сміливо переходити до об’єктів-лініях.

Об’єкти, що складаються з ліній

Також постараємося включити загальне для об’єктів поводження в базовий клас, а саме:
Команда VOC_SIDEMOVE такими об’єктами буде ігноруватися.
Задамо відповідність між вершинами і базовими точками.
type
… / / Базовий клас для об’єктів-ліній
TLineVisualObject = class(TBaseVisualObject)
protected
function GetVertexesCount: Integer; override;
function GetVertex(Index: Integer): TFloatPoint; override;
procedure SetVertex(Index: Integer; const Value: TFloatPoint); override;
end;

implementation

{ TLineVisualObject }
function TLineVisualObject.GetVertex(Index: Integer): TFloatPoint;
begin
Result := BasePoints[Index];
end;
function TLineVisualObject.GetVertexesCount: Integer;
begin
Result := BasePointsCount;
end;
procedure TLineVisualObject.SetVertex(Index: Integer;
const Value: TFloatPoint);
begin
BasePoints[Index] := Value;
end;

Тут все очевидно. Можна відразу переходити до обробки команд VOC_HITTEST і VOC_GETCURSOR.

type
… / / Базовий клас для об’єктів-ліній
TLineVisualObject = class(TBaseVisualObject)
private
procedure VOCHitTest(var Command: TVOCHitTest); message VOC_HITTEST;
procedure VOCGetCursor(var Command: TVOCGetCursor); message VOC_GETCURSOR;
end;

implementation

{ TLineVisualObject }
procedure TLineVisualObject.VOCGetCursor(var Command: TVOCGetCursor);
begin
if Command.HitTest <> HT_OUT then
Command.Result := CR_SIZEALL
else
Command.Result := CR_DEFAULT;
end;
procedure TLineVisualObject.VOCHitTest(var Command: TVOCHitTest);
var
i, sX1, sY1, sX2, sY2: Integer;
D: Extended;
begin
Command.Result := HT_OUT;
for i := VertexesCount – 1 downto 0 do begin / / Переводимо в екранні координати
Command.ConvertIntf.LogToScreen(Vertex[i].X, Vertex[i].Y, sX1, sY1);
if (Abs(Command.Params.XPos – sX1) <= Command.Params.Tolerance) and
(Abs(Command.Params.YPos – sY1) <= Command.Params.Tolerance)
then begin / / Вершина i
Command.Result := HT_VERTEX + i;
Exit;
end;
end; / / Не на лінії чи що?
for i := VertexesCount – 1 downto 1 do begin
Command.ConvertIntf.LogToScreen(Vertex[i].X, Vertex[i].Y, sX1, sY1);
Command.ConvertIntf.LogToScreen(Vertex[i – 1].X, Vertex[i – 1].Y, sX2, sY2);
D := LineDistance(Command.Params.XPos, Command.Params.YPos,
sX1, sY1, sX2, sY2);
if D <= Command.Params.Tolerance then begin / / На лінії
Command.Result := HT_IN + i – 1;
Exit;
end;
end;
end;

В обробнику VOC_HITTEST ми в циклі проходимо по всіх вершин і визначаємо таким же чином, як в прямокутних об’єктах, приналежність точки вершині. Якщо вершина знайдена – виходимо з методу, інакше в наступному циклі перевіряємо відстань від точки до відрізка, що з’єднує (i – 1)-ю і i-ю вершину. Якщо воно не перевищує допустиму похибку, то вважаємо, що точка лежить на лінії. Реалізацію функції LineDistance, яка розраховує відстань від точки до відрізка, ви знайдете у доданих до статті файлах, приводити її тут сенсу немає. До речі, використовуючи подібний цикл по вершинах і функцію LineDistance, ми могли б скоротити код обробки команди VOC_HITTEST у прямокутних об’єктів.


Тепер реалізуємо конструювання об’єктів. Конструювання буде також зводитися до послідовних клацань лівою кнопкою для фіксації положення вершин, а права кнопка – завершить конструювання. При цьому в об’єкта залишаться тільки ті вершини, що ми встигли зафіксувати. Редагована вершина буде віддалятися.


При отриманні команди VOC_CONSTRUCTPOINT – будемо фіксувати положення поточної редагованої вершини і додавати наступну. У цьому ж методі будемо визначати, чи завершено конструювання чи ні. Для сполучної і ламаної лінії тут поведінка буде відмінним. При отриманні команди VOC_PROCESSCONSTRUCT – будемо переміщати поточну крапку в нову позицію. При отриманні команди VOC_STOPCONSTRUCT – будемо завершувати конструювання, видаляти поточну редаговану точку і при необхідності – сигналізувати про те, що об’єкт недобудований.

type
… / / Базовий клас для об’єктів-ліній
TLineVisualObject = class(TBaseVisualObject)

FConstructing: Boolean;
FCurrentPoint: Integer;
procedure VOCConstructPoint(var Command: TVOCConstructPoint);
message VOC_CONSTRUCTPOINT;
procedure VOCProcessConstruct(var Command: TVOCProcessConstruct);
message VOC_PROCESSCONSTRUCT;
procedure VOCStopConstruct(var Command: TVOCStopConstruct);
message VOC_STOPCONSTRUCT;
protected
… / / Визначення моменту завершення конструювання
function NeedToStopConstruct(Count: Integer): Longint; virtual; abstract;
end;

implementation

{ TLineVisualObject }
procedure TLineVisualObject.VOCConstructPoint(
var Command: TVOCConstructPoint);
begin / / Якщо конструювання тільки розпочато – переводимо об’єкт в режим / / Конструювання, встановлюємо початкові параметри і фіксуємо першу точку
if not FConstructing then begin
FConstructing := True;
BeginUpdate;
try
ClearBasePoints;
FCurrentPoint := 0;
AddBasePoint(Command.Pos^.X, Command.Pos^.Y);
finally
EndUpdate;
end;
end; / / Відповідь на питання, чи необхідно завершити конструювання, перекладаємо / / На віртуальний метод NeedToStopConstruct
Command.Result := NeedToStopConstruct(FCurrentPoint + 1);
if Command.Result = 0 then begin;
FConstructing := False;
Exit;
end; / / Додаємо нову точку і змінюємо індекс редагованої
AddBasePoint(Command.Pos^.X, Command.Pos^.Y);
Inc(FCurrentPoint);
end;
procedure TLineVisualObject.VOCProcessConstruct(
var Command: TVOCProcessConstruct);
begin / / Переміщаємо поточну точку
if FConstructing then
BasePoints[FCurrentPoint] := Command.Pos^;
end;
procedure TLineVisualObject.VOCStopConstruct(
var Command: TVOCStopConstruct);
begin
Command.Result := 1;
if FConstructing then begin / / Виходимо з режиму конструювання, видаляємо поточну точку і якщо / / Встановлено менше двох точок – знищуємо об’єкт
FConstructing := False;
DeleteBasePoint(FCurrentPoint);
if VertexesCount < 2 then begin
Free;
Command.Result := 0;
end;
end;
end;

Слід пояснити призначення методу NeedToStopConstruct. Цей метод ми перекриємо в нащадках, щоб задати сполучної і ламаної лінії можливість самостійно визначити, закінчено чи конструювання. При цьому якщо для ламаної лінії цей метод буде завжди повертати код “не закінчено” (єдиний спосіб завершити конструювання ламаної – відправити команду VOC_STOPCONSTRUCT), то сполучна лінія поверне код “закінчено” при установці другої вершини. В цьому випадку, третя вершина до об’єкта добавлена ​​не буде, об’єкт просто вийде з режиму конструювання.


Ну і малювання об’єкта – просто послідовно малюємо всі вершини і з’єднуємо їх лініями:

type
… / / Базовий клас для об’єктів-ліній
TLineVisualObject = class(TBaseVisualObject)
public
procedure Draw(Canvas: TLogicalCanvas); override;
end;

implementation

{ TLineVisualObject }
procedure TLineVisualObject.Draw(Canvas: TLogicalCanvas);
var
i: Integer;
begin
inherited; / / З’єднуємо вершини лініями
for i := 1 to VertexesCount – 1 do
Canvas.DrawLine(Vertex[i – 1].X, Vertex[i – 1].Y, Vertex[i].X, Vertex[i].Y, 0.5);
end;

Реалізація конкретних класів візуальних об’єктів.


Тепер, коли у нас їсть

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*