Реалізація інтерактивних діаграм за допомогою ООП на прикладі прототипу редактора UML-діаграм. Частина 1

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

У цій статті ми детально розберемо створення «з нуля» компоненти з інтерактивними, «перетаскиваемыми» елементами в об'єктно-орієнтованої середовищі розробки. В якості прикладу ми побудуємо прототип UML-редактора.


Постановка завдання
Основні вимоги до виконання
Список завдань, які потребують реалізації інтерактивної графіки, досить великий. Це можуть бути
  • умовні схеми устаткування (установок, конвеєрів, транспортних систем), на яких відображається стан роботи вузлів, «по клацанню миші» користувач бажає отримувати додаткову інформацію або вводити керуючі команди,
  • аналітичні графіки (гістограми, бульбашкові діаграми), «по клацанню миші» на елементи яких користувач бажає отримувати інформацію, або ж користувач бажає напряму «підтягати» мишею елементи, змінюючи картину в потрібну сторону, щоб дізнатися необхідні числові показники,
  • візуальний аналіз даних, представимых у вигляді графів (наприклад: взаємозв'язки між юридичними особами в базі даних), користувач бажає «перетягувати» елементи графа вручну, щоб у підсумку вставити отриману картинку в друкований звіт,
  • всі засоби візуального моделювання/конструювання чого-небудь з блоків, зокрема, всі CASE-засоби,
— і так далі, і тому подібне. Хоча зовнішній вигляд усіх цих діаграм різний, у всіх випадках потрібно реалізовувати деякі загальні вимоги. Ось вони:
  • Картинка повинна складатися з дискретних елементів різної графічної складності,
  • Картинка повинна бути масштабованої і прокручувати, тобто користувач повинен мати можливість побільше розгледіти будь-який з фрагментів діаграми, використовуючи зміна масштабу і смуги прокрутки,
  • Деякі з елементів картинки повинні бути «клікабельними», тобто система в кожний момент має «розуміти», на який саме елемент навести вказівник миші, мати можливість показувати для них спливаючі підказки,

  • Деякі «клікабельних» елементів повинні бути «виділяються», тобто користувач повинен мати можливість «поставити виділення» на елемент клацанням миші, повинно бути доступно виділення групи елементів з натиснутою клавішею Shift і за допомогою «прямокутного ласо». З виділеними об'єктами, в залежності від завдання, можуть проводитися деякі дії або зміна властивостей.
  • Деякі «клікабельних» елементів повинні бути «перетаскиваемыми», тобто користувач повинен мати можливість пересунути мишею один елемент або групу виділених елементів:

Важливість можливості «перетягування» елементів слід підкреслити особливо. Якщо ваше завдання включає в себе необхідність візуалізації графів, потрібно пам'ятати: ні один з численних алгоритмів автоматичного розташування вузлів графа на площині не може дати рішення, повністю задовільний у всіх випадках, і для зручності користувача «ручна» перестановка вузлів графа просто необхідна.
Які ж «ингриденты» потрібні для приготування цього «страви»? У цій статті ми покажемо загальні принципи, які можна застосовувати в будь-якому середовищі розробки при виконанні всього чотирьох ключових умов:
  1. Об'єктно-орієнтована мова програмування.
  2. Доступність об'єкта-«полотна» (Canvas), з можливістю малювання графічних примітивів (ліній, дуг, багатокутників і т. п.).
  3. Компоненти, що реалізують керовані смуги прокручування.
  4. Доступність обробки подій миші.
Наш ілюструє приклад являє собою прототип редактора UML Use Case-діаграм, ми будемо користуватися красивою діаграмою з цього керівництва. Вихідні коди нашого прикладу доступні за адресою http://inponomarev.ru/programming/graph.zip і можуть бути скомпільовані за допомогою Maven. Якщо ви хочете краще засвоїти викладені в статті принципи, я настійно рекомендую завантажити ці вихідні коди і вивчати їх разом зі статтею.

Приклад побудований на Java 8 зі стандартної бібліотеки Swing. Однак викладених у принципах немає нічого Java-специфічного. Ми вперше реалізували викладені тут принципи в Delphi (Windows-додатка), а потім в Google Web Toolkit (веб-додатки з висновком графіки на HTML Canvas). При виконанні чотирьох вищевказаних умов запропонований приклад можна сконвертувати в іншу середовище розробки.

Труднощі «наївного» підходу
Взагалі, намалювати якусь схему на екрані, використовуючи методи виведення графічних примітивів — завдання на зразок неважка. Палиця, палиця, огуречик (з подібного вправи на мові BASIC коли-то давно я вперше познайомився з програмуванням):
canvas.drawOval(10, 0, 10, 10);
canvas.drawLine(15, 10, 15, 25);
canvas.drawLine(5, 15, 25, 15);
canvas.drawLine(5, 35, 15, 25);
canvas.drawLine(25, 35, 15, 25);


Але поки що наш «чоловічок» не «ожив»: його не можна виділяти, масштабувати, переміщати по полотну, він не вміє взаємодіяти з суспільством інших «чоловічків». Значить, треба написати код, відповідальний за всі ці операції. Для простої картинки це здається нескладним, однак по мірі ускладнення того, що ми хочемо отримати, нас чекають проблеми.
  • З ускладненням картини зростає довжина «процедури малювання». Для складної схеми процедура стає дуже довгою і заплутаною.
  • Код, який малює картинку, сам «чари» не задає критерій, за яким можна було б визначити об'єкт, що виділяється в поточний момент курсором миші. Ми повинні писати окрему процедуру, що визначає об'єкт, над яким знаходиться вказівник миші, і при цьому повинні постійно синхронізувати код процедури відтворення і процедури розпізнавання об'єктів.
  • Разом зі складністю процедури відтворення зростає складність процедури розпізнавання.
Спроба вирішити завдання «в лоб» ускладненням процедур приречена на провал в силу швидкого ускладнення вихідного коду, кількість якого буде зростати лавиноподібно по мірі ускладнення діаграми. Однак застосування об'єктно-орієнтованої розробки, універсальний принцип «розділяй і володарюй», а також патерни проектування дають нам досить потужною інструментарій, щоб витончено розправитися з перерахованими проблемами і реалізувати потрібну функціональність.

Отже, ми приступаємо до виконання.

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



І складемо наступну ієрархію:
  • Діаграму
    • Ролі (Actors)
      • Підпису ролей
    • Варіанти використання (Use Cases)
    • Успадкування (generalizations)
    • Зв'язку (associations)
    • Залежності (dependencies)
      • Підпису стереотипів залежностей



Наш приклад, насправді, дуже проста, тому ієрархія вийшла неглибокою. Чим складніше малюнок, тим ширшою і глибшою буде ієрархія.

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

Кожному з пунктів цієї ієрархії буде відповідати клас-отрисовщик, а ієрархічна зв'язок між ними дозволяє застосувати патерн Composite — «конструктор», який (цитую книгу «Design Patterns») «компонує об'єкти в деревовидні структури для подання ієрархій частина-ціле, дозволяє… одноманітно трактувати індивідуальні та складові об'єкти». Тобто робить те, що нам потрібно.

На діаграмі класів наша система має наступний вигляд:


У верхній частині діаграми класів перебувають два класи (DiagramPanel і DiagramObject), які «нічого не знають» про конкретику отрисовываемой діаграми і утворюють фреймворк, на основі якого можна робити діаграми різного виду. DiagramPanel (в нашому випадку це спадкоємець класу javax.swing.JPanel) являє собою візуальний компонент інтерфейсу, відповідальний за відображення діаграми та її взаємодія з користувачем. Об'єкт DiagramPanel містить в собі посилання на DiagramObject — кореневий об'єкт-отрисовщик, відповідний найвищим рівнем ієрархії відтворення (в нашому випадку це буде екземпляр класу UseCaseDiagram).

DiagramObject — це базовий клас всіх об'єктів-отрисовщиков, реалізує їх ієрархію через патерн Composite і багато іншого, про що мова піде далі.

У нижній частині знаходиться приклад використання фреймворку. Клас Example (спадкоємець javax.swing.JFrame) — це головне вікно програми, яке в нашому прикладі містить у собі в якості однієї єдиної компоненти примірник DiagramPanel. Всі інші класи — спадкоємці DiagramObject. Вони відповідають завданням в ієрархічному переліку відтворення. Зверніть увагу, що ієрархія наслідування цих класів і ієрархія відтворення — це різні ієрархії!

Ієрархія відтворення, у відповідності зі сказаним вище, виглядає так:

  • UseCaseDiagram — діаграму,
    • DiagramActor — роль,
      • Label — підпис ролі,
    • DiagramUseCase — варіант використання,
    • DiagramGeneralization — спадкування,
    • DiagramAssociation — зв'язок,
    • DiagramDependency — залежність,
      • Label — підпис стереотипу залежності.



Далі ми докладно опишемо пристрій класів DiagramObject і DiagramPanel і те, як їх слід використовувати.

Клас DiagramObject і його спадкоємці
Структура даних
Клас DiagramObject влаштований так, що всередині кожного з його примірників знаходиться двусвязный список підпорядкованих отрисовщиков. Це досягається за допомогою змінних previous, next, first і last, що дозволяють посилатися на сусідні елементи в списках та ієрархії. Коли об'єкти инстанцированы, виходить приблизно така картина:



Ця, подібна простому двусвязному списком, структура даних хороша тим, що ми можемо за час O(N) зібрати потрібну нам ієрархію, а при необхідності — за час О(1) і модифікувати її, видаливши заданий елемент або вставивши новий в список після будь-якого заданого елемента. Доступ до елементів цієї структури нас цікавить тільки послідовний, відповідний обходу дерева в глибину, що досягається проходом по посиланнях. Рух по червоним стрільцям відповідає обходу в пряму, а з синім стрільцям — траверс у зворотний бік.

Для додавання нового об'єкта у внутрішній список DiagramObject служить метод addToQueue(DiagramObject subObj):
if (last!=null) {
last.next = subObj;
subObj.previous = last;
} else {
first = subObj;
subObj.previous = null;
}
subObj.next = null;
subObj.parent = this;
last = subObj;


Щоб зібрати бажану картину, залишається лише проинстанцировать потрібну кількість потрібних отрисовщиков і об'єднати їх в черзі в потрібному порядку. У нашому прикладі більша частина цієї роботи відбувається в конструкторі класу UseCaseDiagram:
DiagramActor a1 = new DiagramActor(70, 150, "Customer");
addToQueue(a1);
DiagramActor a2 = new DiagramActor(50, 350, "NFRC Customer");
addToQueue(a2);
DiagramActor a3 = new DiagramActor(600, 50, "Bank Employee");
addToQueue(a3);
...
DiagramUseCase uc1 = new DiagramUseCase(250, 50, "Open account");
addToQueue(uc1);
DiagramUseCase uc2 = new DiagramUseCase(250, 150, "Deposit funds");
addToQueue(uc2);
...
addToQueue(new DiagramAssociation(a1, uc1));
addToQueue(new DiagramAssociation(a1, uc2));
...
addToQueue(new DiagramDependency(uc2, uc5, DependencyStereotype.EXTEND));
addToQueue(new DiagramDependency(uc2, uc6, DependencyStereotype.INCLUDE));
...
addToQueue(new DiagramGeneralization(a2, a1));


У реальному житті слід чинити, звичайно, не так: замість «зашивання код» процесу створення об'єктів-отрисовщиков, конструктор базового класу вам необхідно буде передати модель даних вашої системи. Обходячи вже в циклах об'єкти цієї моделі, ви будете створювати отрисовщики. Наприклад, для кожного пов'язаного з поточною діаграмою екземпляра класу Actor (відповідного «ролі» у вашій «моделі документа UML») необхідно проинстанцировать об'єкт класу-отрисовщика DiagramActor.

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

Світові і екранні координати
Коль скоро ми застосували термін «світові координати» — потрібно уточнити, що це таке в нашому випадку. «Світові координати» у нас — це координати об'єктів діаграми на «уявної міліметровому папері», на якій уміщається діаграму, яка має початок координат в лівому верхньому куті і не піддається ніякому масштабуванню. Світові координати збігаються з екранними, якщо масштаб картинки 1:1 і смуги прокрутки знаходяться у своїх мінімальних позиціях. Світова координата, на відміну від екранної, має не цілочисельний тип, а приймає значення з що плаває крапкою. Це потрібно, щоб не відбувалася пікселізация картинки при збільшенні її масштабів. Наприклад, при масштабі 1:1 значення світової координати 0.3 не відрізна від нуля екранних пікселів, в масштабі 100:1 воно перетворюється вже в 30 екранних пікселів.

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

Для перекладу світових координат в екранні клас DiagramObject містить важливі методи scaleX(...), scaleY(...) і scale(...). Перші два застосовують до світової координаті масштабний коефіцієнт і враховують зрушення горизонтальної і вертикальної смуги прокручування, відповідно. Останній метод, scale(...), застосовує масштабний коефіцієнт, але не враховує зсув: він необхідний для розрахунку не позиції, а розміру (наприклад, ширини прямокутника або радіусу кола).

Побудова діаграми з точки зору DiagramObject. Самостійні, напів-самостійні і залежні об'єкти

Для відображення діаграми викликається метод draw Graphics canvas, double aDX, double aDY, double scale) кореневого DiagramObject. Його параметрами є:
  • canvas — контекст малювання
  • aDX, aDY — положення смуг прокрутки
  • scale — масштаб (1.0 — для масштабу 1:1, не більше/менше — для збільшення/зменшення).
Цей метод реалізує шаблон проектування Template Method (шаблонний метод) і виглядає наступним чином:
this.canvas = canvas;
this.scale = scale;
dX = aDX;
dY = aDY;
saveCanvasSetup();
internalDraw(canvas);
restoreCanvasSetup();
DiagramObject curObj = first;
while (assigned(curObj)) {
curObj.draw(canvas, aDX, aDY, scale);
curObj = curObj.next;
}


Тобто метод draw(...):
  • запам'ятовує в полях об'єкта параметри (вони потім неодноразово використовуються різними методами),
  • зберігає за допомогою saveCanvasSetup() всі налаштування контексту відтворення (колір, пір'я, розмір шрифту тощо),
  • викликає метод internalDraw(), який на рівні DiagramObject не робить нічого, а в його спадкоємців змінюється процедурою відтворення об'єкта,
  • відновлює з допомогою restoreCanvasSetup() налаштування, які могли бути порушені після виконання internalDraw,
  • пробігає по черзі всіх своїх подобъектов і викликає метод draw для кожного з них.
Таким чином, інваріантна частина алгоритму реалізована в методі draw(...), а змінна частина (власне малювання) реалізується в класах-спадкоємців, що і становить суть патерну Template Method.

Призначення методів saveCanvasSetup() і restoreCanvasSetup() — зберегти стан контексту малювання, так щоб кожен з об'єктів-отрисовщиков отримав його в «незайманому» вигляді. Якщо ці методи не застосовувати в одному з спадкоємців-отрисовщиков, припустимо, колір чорнила змінити на червоний, то все, що буде намальовано далі, буде намальовано червоним кольором. Реалізація даних методів залежить від вашого середовища розробки і можливостей, що надаються механізмом малювання. У Delphi і Java Swing, приміром, треба зберігати безліч параметрів контексту, а в HTML Canvas2D спеціально для цієї мети є готові методи save() і restore(), відразу зберігають у спеціальний стек все стан контексту.

Ось як виглядає метод internalDraw в класі DiagramActor (порівняйте з «наївним прикладом», з якого ми почали):
static final double ACTOR_WIDTH = 25.0;
static final double ACTOR_HEIGHT = 35.0;

@Override
protected void internalDraw(Graphics canvas) {
double mX = getmX();
double mY = getmY();
canvas.drawOval(scaleX(mX + 10 - ACTOR_WIDTH / 2), scaleY(mY + 0 - ACTOR_HEIGHT / 2), scale(10), scale(10));
canvas.drawLine(scaleX(mX + 15 - ACTOR_WIDTH / 2), scaleY(mY + 10 - ACTOR_HEIGHT / 2),
scaleX(mX + 15 - ACTOR_WIDTH / 2), scaleY(mY + 25 - ACTOR_HEIGHT / 2));
canvas.drawLine(scaleX(mX + 5 - ACTOR_WIDTH / 2), scaleY(mY + 15 - ACTOR_HEIGHT / 2),
scaleX(mX + 25 - ACTOR_WIDTH / 2), scaleY(mY + 15 - ACTOR_HEIGHT / 2));
canvas.drawLine(scaleX(mX + 5 - ACTOR_WIDTH / 2), scaleY(mY + 35 - ACTOR_HEIGHT / 2),
scaleX(mX + 15 - ACTOR_WIDTH / 2), scaleY(mY + 25 - ACTOR_HEIGHT / 2));
canvas.drawLine(scaleX(mX + 25 - ACTOR_WIDTH / 2), scaleY(mY + 35 - ACTOR_HEIGHT / 2),
scaleX(mX + 15 - ACTOR_WIDTH / 2), scaleY(mY + 25 - ACTOR_HEIGHT / 2));
}


В точці (mX, mY) знаходиться середина об'єкта. Т. к. початок координат «наївного прикладу» знаходиться в лівому верхньому кутку, їх необхідно змістити на половину ширини і половину висоти об'єкта. «Наївний приклад» не враховував необхідність масштабування і зсуву картинки, ми враховуємо це, переводячи світові координати в екранні за допомогою методів scaleX(...), scaleY(...) і scale(...).

Обєкти DiagramActor і DiagramUseCase повністю «самостійні», їх позиції цілком визначаються внутрішнім станом, зберігаються в полях mX і mY. У той же час всілякі сполучні стрілки власного стану не мають — їх позиція на екрані повністю визначена позиціями об'єктів, які вони з'єднують, вони повністю не самостійні», вони проходять по прямій, що з'єднує центри об'єктів:



І окремо слід звернути увагу на підписи до об'єктів. У своєму внутрішньому стані вони зберігають не абсолютні координати, а зміщення відносно батьківського об'єкта-отрисовщика, тому вони ведуть себе «напів-самостійно»:



Визначення об'єкта під курсором миші
Розібравшись з промальовкою, переходимо до питання про те, яким же чином діаграма «розуміє», на який об'єкт ми кликнули мишею. Виявляється, що задача визначення об'єкту, що знаходиться під курсором миші, дуже схожа на завдання відтворення і в певному сенсі симетрична їй.

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

Наприклад, для DiagramActor мова йде про попаданні в прямокутну область:
protected boolean internalTestHit(double x, double y) {
double dX = x - getmX();
double dY = y - getmY();
return dY > -ACTOR_HEIGHT / 2 && dY < ACTOR_HEIGHT / 2 
&& dX > -ACTOR_WIDTH / 2 && dX < ACTOR_WIDTH / 2;
}

Для DiagramUseCase мова йде про попадання в область, що має вигляд еліпса:
protected boolean internalTestHit(double x, double y) {
double dX = 2 * getScale() * (x - getmX()) / (width + 2 * MARGIN / getScale());
double dY = 2 * (y - getmY()) / HEIGHT;
return dX * dX + dY * dY <= 1;
}

Тепер, якщо ми хочемо визначити об'єкт, над яким зараз знаходиться курсор, ми можемо методом послідовного перебору викликати internalTestHit для кожного з об'єктів діаграми, і перший, повернула true, виявиться потрібним об'єктом. Тільки робити це треба в порядку, зворотному до порядку відтворення (рух за синім стрілками на ілюстрації, що показують структуру даних)! Якщо курсор знаходиться в області, на якій перетинається кілька об'єктів, саме пошук у зворотному порядку забезпечить потрапляння курсором в об'єкт, отрисованный пізніше інших, тобто візуально знаходиться «над іншими».



Ось як це реалізовано в ще одному шаблонному методі DiagramObject:
public final DiagramObject testHit(int x, int y) {
DiagramObject result;
DiagramObject curObj = last;
while (assigned(curObj)) {
result = curObj.testHit(x, y);
if (assigned(result))
return result;
curObj = curObj.previous;
}
if (internalTestHit(x / scale + dX, y / scale + dY))
result = this;
else {
result = null;
}
return result;
}


Об'єкт DiagramPanel викликає метод testHit кореневого об'єкта відтворення. Під час його виконання відбувається рекурсія, яка виконує обхід дерева відтворення в глибину в напрямку, протилежному напрямку відтворення. Повертається перший знайдений об'єкт: це і буде самий «верхній» з точки зору користувача об'єкт, що знаходиться під курсором миші.

Визначення поточного контексту під курсором миші
Об'єкт, що знаходиться під курсором миші, може виявитися лише складовою частиною більш великого об'єкта і не мати самостійного значення. Якщо ми хочемо провести певну операцію над об'єктом і кликнули мишею на його частина, то операцію все одно потрібно робити над батьківським об'єктом. Правильно показати контекст можна за допомогою делегування — прийому, пов'язаного з використанням шаблону Composite (див. на цей рахунок книгу Design Patterns). У нашому прикладі ми застосовуємо делегування для отримання підказки об'єкта: наприклад, якщо користувач наводить вказівник миші на підпис під Actor-му, він отримує ту ж підказку, що і при наведенні курсору власне на Actor-а.

Ідея дуже проста: метод getHint() класу DiagramObject виконує наступне: якщо його власна реалізація методу internalGetHint() в змозі повернути рядок-підказку, то вона ж і повертається. Якщо не в змозі, то йде звернення до батьківського (в ієрархії відтворення) об'єкта — не може він виконати роботу методу getHint(). У разі, якщо він «не береться», «передача відповідальності» буде тривати до самого кореневого об'єкта-отрисовщика. Крім механізму делегування, ми знову застосовуємо патерн Template Method:
public String getHint() {
StringBuilder hintStr = new StringBuilder();
if (internalGetHint(hintStr))
return hintStr.toString();
else if (assigned(parent))
return parent.getHint();
else {
return "";
}
}
protected boolean internalGetHint(StringBuilder hintStr) {
return false;
}

Допоміжні методи DiagramObject
Спадкоємці DiagramObject можуть перевизначити наступні методи їх використання в класі DiagramPanel стане зрозуміло з подальшого:

  • boolean isCollectable() — можна буде захопити об'єкт з допомогою «ласо» (прямокутного виділення). Використовується механізмами DiagramPanel, про яких мова піде далі
  • boolean isMoveable() — є об'єкт переміщуються за допомогою Drag and Drop. У нашому прикладі вузли діаграми (Actor і UseCase) є переміщуються і захоплюваними за допомогою ласо, а з'єднувальні лінії (Association, Generalization, Dependency) такими не є.
  • double getMinX(), getMinY(), getMaxX(), getMaxY() — світові координати самої лівої, самого верхнього, правого і самої нижньої точки об'єкта. Потрібні, по-перше, для коректної роботи прямокутного виділення (щоб виділити об'єкт, потрібно захопити його цілком), а по-друге, вони використовуються у дефолтної реалізації методу internalDrawSelection(), щоб намалювати виділення об'єкта по його кутах.
  • final int minX(), minY(), maxX(), maxY() — те ж саме, але вже перекладене в екранні координати (не переопределяемые методи).
Отрисовка виділення
І нарешті, ще одна важлива функціональність, реалізована на рівні DiagramObject, яка може бути перевизначено у його спадкоємців — вивід виділення, т. е. графічної позначки, за якою користувач може зрозуміти, що об'єкт знаходиться в активному стані. За замовчуванням це чотири сині квадратні точки по кутах об'єкта:

private static final int L = 4;

protected void internalDrawSelection(Graphics canvas, int dX int dY) {
canvas.setColor(Color.BLUE);
canvas.setXORMode(Color.WHITE);
canvas.fillRect(minX() + dX - L, minY() + dY - L, L, L);
canvas.fillRect(maxX() + dX, minY() + dY - L, L, L);
canvas.fillRect(minX() + dX - L, maxY() + dY, L, L);
canvas.fillRect(maxX() + dX, maxY() + dY, L, L);
canvas.setPaintMode();
}

Зверніть увагу на цілочисельні (а значить, в екранних координатах) параметри dX, dY і на виклик setXORMode(), перемикаючий контекст відтворення в «XOR-режим»: у цьому режимі для того, щоб стерти раніше намальоване зображення, досить виділити його ще раз. Це потрібно для того, щоб реалізувати Drag&Drop для об'єктів діаграми: для простоти ми перетягуємо» мишею не саме зображення, а його виділення, і потім вже перебрасываем зображення на нове місце, при цьому в параметрах dX, dY буде передано зміщення об'єкта в екранних координатах відносно вихідного положення:


Якщо така поведінка системи не влаштовує, то можна перевизначити метод internalDrawSelection у спадкоємців класу DiagramObject, щоб малювати в якості виділення (і пересувати при drag&drop) що-небудь більш складне.

* * *
Це все, що стосується класу DrawObject. У другій частині статті буде розглянуто побудову класу DiagramPanel, відповідального за обробку подій миші та масштабування, панорамування, виділення об'єктів і drag&drop. Повний вихідний код нашого прикладу, нагадую, доступний за адресою http://inponomarev.ru/programming/graph.zip і може бути скомпільований за допомогою Maven.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.