Итак, мои уважаемые читатели, я продолжаю рассматривать животрепещущую тему строительства самолепного,
простого векторного графического редактора.
2. Перемещение фигур
Для перемещения фигур у нас пока задействована только мышка и мы будем рассматривать взаимодействие пользователя
с фигурами через события _container.MouseDown, _container.MouseMove и _container.MouseUp. Я об этом не упоминал ранее,
а теперь скажу, что имеется конструктор класса Picture, в котором и производится привязка событий контейнера к нашим
обработчикам событий:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| public Picture(Control container)
{
_container = container;
// подключаемся к необходимым событиям на контейнере
_container.MouseDown += ContainerMouseDown;
_container.MouseMove += ContainerMouseMove;
_container.MouseUp += ContainerMouseUp;
_container.Paint += ContainerPaint;
// подключаем объект выбора рамкой
_ribbonSelector = new RectangleRibbonSelector(_container);
_ribbonSelector.OnSelected += RibbonSelectorOnSelected;
// а здесь пробуем найти ссылку на форму, на которой расположен PaintBox
var parent = _container.Parent;
// пока не найдём форму или пустой Parent
while (!(parent is Form))
{
if (parent == null) break;
parent = parent.Parent;
}
_form = parent as Form;
// если найдена форма
if (_form != null)
{
// то подключим к ней обработчик нажатия клавиш
_form.KeyDown += FormKeyDown;
_form.KeyUp += FormKeyUp;
// включим признак предварительного просмотра нажатия клавиш
_form.KeyPreview = true;
}
// при изменении выбора выключаем режим изменения узлов
_selected.CollectionChanged += (sender, args) =>
{
NodeChanging = false;
};
} |
|
В строках с 5 по 8 подключаем в цепочку вызовов собственные обработчики событий на нажатие кнопок мышки и
её перемещение, а также обработчик события рисования на канве контейнера.
В строках 10 и 11 создаём объект для выбора рамкой и подключаем ему обработчик события:
Обработчик события завершение выбора прямоугольником
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| /// <summary>
/// Обработчик события завершение выбора прямоугольником
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void RibbonSelectorOnSelected(object sender, RibbonSelectedEventArgs e)
{
if (EditorMode != EditorMode.Selection) return;
// нормализация параметров для прямоугольника выбора
var rect = e.RectangleSelected;
// добавляем все фигуры, которые оказались охваченными прямоугольником выбора
// в список выбранных фигур
foreach (var fig in _figures.Where(fig => rect.Contains(Rectangle.Ceiling(fig.GetBounds))))
_selected.Add(fig);
} |
|
В строках с 13 по 20 пробуем среди череды предков (Parent) найти компонент типа Form, у которого имеются
события нажатия клавиш на клавиатуре _form.KeyDown и _form.KeyUp, к которым мы также подключаем
собственные обработчики событий в строках 25 и 26 для сохранения состояния управляющей клавиши Ctrl,
и которое используется в алгоритме множественного выбора фигур.
В строке 28 мы включаем у контейнерной формы режим пред-просмотра нажатия клавиш на контролах формы.
Вот эти обработчики:
Обработчики событий нажатия клавиш клавиатуры
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| /// <summary>
/// Во внешней форме нажата клавиша
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void FormKeyDown(object sender, KeyEventArgs e)
{
// проверяем нажатие Ctrl
if (e.Control) _controlPressed = true;
}
/// <summary>
/// Во внешней форме отпущена клавиша
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void FormKeyUp(object sender, KeyEventArgs e)
{
// проверяем отпускание Ctrl
if (!e.Control) _controlPressed = false;
} |
|
В строках с 31 по 34 мы программируем событие изменения списка выбора, где сбрасывается режим изменения узлов
NodeChanging.
При выборе фигур мышкой они попадают в список выбранных, точка нажатия запоминается в переменной _mouseDownLocation,
а переменная смещения координат мышки _mouseOffset относительно _mouseDownLocation - обнуляется.
Если была нажата левая кнопка мышки, то производится захват мышки контейнером _container.Capture = true;
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| /// <summary>
/// Нажатие кнопки "мышки" на PaintBox
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ContainerMouseDown(object sender, MouseEventArgs e)
{
// запоминаем точку первую точку для прямоугольника выбора
_mouseDownLocation = e.Location;
_mouseOffset = Point.Empty;
// если установлен другой режим, кроме выбора прямоугольником
if (EditorMode != EditorMode.Selection)
_selected.Clear(); // очищаем список выбранных
Figure fig;
_markerIndex = PointInMarker(e.Location, out fig);
// ищем фигуру в точке нажатия
if (fig == null) fig = PointInFigure(e.Location);
if (e.Button == MouseButtons.Left)
_container.Capture = true;
...
} |
|
Когда пользователь "тащит" фигуру мышкой, левая кнопка удерживается нажатой и при этом пересчитывается величина
смещения курсора мышки относительно точки первоначального нажатия в переменной _mouseOffset.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| /// <summary>
/// Перемещение мышки над PaintBox
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ContainerMouseMove(object sender, MouseEventArgs e)
{
if (_mouseDownLocation == e.Location) return;
// если удерживается левая кнопка и мышка захвачена
if (e.Button == MouseButtons.Left && _container.Capture)
{
// пересчитываем смещение мышки
_mouseOffset.X = e.X - _mouseDownLocation.X;
_mouseOffset.Y = e.Y - _mouseDownLocation.Y;
// просим перерисовать
_container.Invalidate();
}
if (e.Button != MouseButtons.None) return;
...
} |
|
Когда мы просим _container.Invalidate(), то в событии ContainerPaint для всех выбранных фигур
вызывается метод DrawFocusFigure():
C# | 1
2
3
4
5
6
7
8
| private void ContainerPaint(object sender, PaintEventArgs e)
{
...
// при перетаскивании
if (EditorMode != EditorMode.Dragging) return;
foreach (var fig in _selected)
DrawFocusFigure(fig, e.Graphics, _mouseOffset, _markerIndex, NodeChanging);
} |
|
В методе DrawFocusFigure() при перетаскивании фигуры происходит следующее:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| ....
using (var gp = new GraphicsPath())
{
if (figure.Kind == DrawingKind.Polygon)
// добавляем в полигон все точки фигуры
gp.AddPolygon(figure.GetPoints());
else
gp.AddLines(figure.GetPoints());
// получаем графический путь
var ps = gp.PathPoints;
// для всех точек пути
for (var i = 0; i < ps.Length; i++)
{
// делаем смещение
ps[i].X += offset.X;
ps[i].Y += offset.Y;
}
DrawCustomFigure(graphics, ps, figure.Kind);
}
.... |
|
По точкам фигуры строится графический путь (строки 4-8), из этого пути берётся реальный массив точек для рисования (строка 10) и
в цикле всем точкам передаётся текущее смещение мышки на данный момент (строки 12-17), ну а метод DrawCustomFigure()
просто рисует фигуру по точкам, пунктиром, на новом месте.
То есть реального перемещения фигур в событии ContainerMouseMove не делается, а только показывается их будущее положение.
Фиксация фигур на новых местах происходит при обработке события ContainerMouseUp().
Когда перетаскивание фигур заканчивается, то для всех выбранных фигур (из списка _selected) выполняется метод UpdateLocation(fig, _mouseOffset),
который фиксирует положение фигур на новом месте:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| /// <summary>
/// Перемещение фигуры
/// </summary>
/// <param name="fig">объект фигуры</param>
/// <param name="offset">смещение</param>
public void UpdateLocation(Figure fig, PointF offset)
{
// перемещение фигуры
var pts = fig.GetPoints();
var oldrect = CalcFocusRect(PointF.Empty, fig, 0);
var newrect = CalcFocusRect(offset, fig, 0);
for (var i = 0; i < pts.Length; i++)
{
pts[i].X = newrect.Left + (pts[i].X - oldrect.Left);
pts[i].Y = newrect.Top + (pts[i].Y - oldrect.Top);
}
fig.SetPoints(pts);
} |
|
В стоке 9 все точки фигуры собираются в массив, затем при помощи метода CalcFocusRect() по этому массиву точек
строятся два охватывающего прямоугольника, и в цикле для всех точек производится коррекция положения.
В строке 17 массив точек заливается обратно в фигуру.
Так фигура у нас перемещается на новое место.
Я всё думаю о том, что такая модель хранения абсолютных координат точек в фигуре не совсем хороша.
Как-то не по феншую... Было бы лучше хранить не самые координаты точек, а их смешения относительно некой одной
точки (Origin) и алгоритм рисования делать исходя из этого. Тогда достаточно было бы смещать координаты только одной
этой точки, не трогая остальной массив.
С другой стороны, при изменении размеров фигуры пересчёт координат всех точек производить бы пришлось...
С перемещением фигур также закончили.
Но, чем дальше, тем интереснее: нас ждут изменение размеров и изменение узловых точек.
До встречи! |