Visual C++: перетаскиваем много объектов и проверяем их пересечения
Подобно тому, как мы двигали по канве одну картинку,
можно организовать и движение нескольких, если хранить их список или просто просматривать список контролов (Controls
) формы.
Хотелось бы добавить в пример и дополнительные возможности, например, запретить картинкам накладываться друг на друга.
Основные проблемы, которые при этом возникнут, связаны с "неожиданными" цепочками обработчиков событий, например, во время "длинной" проверки на пересечение цепочки прямоугольников в методе MouseMove
, координаты курсора мыши из-за резкого движения ею успевают измениться, и возникает коллизия - картинки, которые не должны были пересекаться, оказываются наложенными одна на другую.
Решать это можно дополнительными проверками и ограничениями, например, прекращая обработку событий, если установлен некий флажок "занято".
В классе формы опишем нужные данные:
static const int OFFSET = 1; //смещение при управлении клавиатурой static bool isBusy=false; //флажок "занято" для исключения лишней обработки событий List <PictureBox ^> ^pictureBoxes; //список картинок PictureBox ^ activePBox; //указатель на активную картинку Point clickPoint; //координаты клика мышью
Для работы списка к проекту также подключен дополнительный namespace:
using namespace System::Collections::Generic;
В конструкторе формы, после вызова метода InitializeComponent()
, нам понадобится инициализировать 3 величины:
pictureBoxes = gcnew List <PictureBox ^>(); activePBox = nullptr; clickPoint = Point(0, 0);
Добавим на форму также стандартное меню menuStrip1
с пунктами "Добавить" и "Удалить", плюс статусную строку окна statusStrip1
с меткой toolStripStatusLabel1
на ней.
Основной метод приложения будет называться PictureBox ^ AddNewPictureBox(int x, int y)
, его цель - создать очередную картинку типа PictureBox
по координатам левого верхнего угла (x, y)
:
private: PictureBox ^ AddNewPictureBox(int x, int y) { PictureBox ^pictBox = gcnew PictureBox(); pictBox->Width = 100; pictBox->Height = 100; pictBox->Image = gcnew Bitmap(pictBox->Width, pictBox->Height); pictBox->Location = Point(x, y); pictBox->Paint += gcnew PaintEventHandler(this, &Form1::pictureBox_Paint); pictBox->LocationChanged += gcnew EventHandler(this, &Form1::pictureBox_LocationChanged); pictBox->MouseDown += gcnew MouseEventHandler(this, &Form1::pictureBox_MouseDown); pictBox->MouseMove += gcnew MouseEventHandler(this, &Form1::pictureBox_MouseMove); pictBox->PreviewKeyDown += gcnew PreviewKeyDownEventHandler(this, &Form1::pictureBox_PreviewKeyDown); pictureBoxes->Add(pictBox); Controls->Add(pictBox); Random ^ rnd = gcnew Random(); Color clr = Color::FromArgb(rnd->Next(0,255),rnd->Next(0,255),rnd->Next(0,255)); pictBox->BackColor = clr; //Как ниже не делаем - рисовать на Graphics нужно в обработчике события Paint /* Graphics ^gr = Graphics::FromImage(pBox->Image); SolidBrush ^ brush = gcnew SolidBrush (clr); gr->FillRectangle(brush, Rectangle(pBox->Location, pBox->Size)); */ return pictBox; }
Обратите внимание на то, что метод ставит создаваемой картинке все нужные обработчики событий, одинаковые для всех объектов.
Также избежим распространённой ошибки начинающих и не будем пытаться рисовать на канве объекта вне его метода Paint
, взгляните на закомментированный код. А вот поставить фоновый цвет (свойство BackColor
) - пожалуйста, можно и здесь.
Метод Paint
демонстрирует, как можно рисовать внутри созданной картинки, мы ограничимся красной рамкой:
private: System::Void pictureBox_Paint (System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) { //Здесь можно получить канву из e->Graphics и рисовать на ней в прямоугольнике e->ClipRectangle: if (isBusy) return; isBusy = true; Pen ^ MyPen = gcnew Pen(Color::Red); Graphics ^gr = e->Graphics; Rectangle clipRectangle = e->ClipRectangle; gr->DrawRectangle(MyPen, clipRectangle.X, clipRectangle.Y, clipRectangle.Width-1, clipRectangle.Height-1); isBusy = false; }
Здесь и далее обработка события прекращается, если класс находится в состоянии "занято" - а находиться он в нём будет, прежде всего, при выполнении "длинного" кода в обработчике перетаскивания картинки мышью.
Метод обработки события LocationChanged
просто выведет координаты левого верхнего угла картинки в статусную строку, служебный drawXY
для этой цели напишем позднее:
private: System::Void pictureBox_LocationChanged (System::Object^ sender, System::EventArgs^ e) { if (isBusy) return; isBusy = true; drawXY((PictureBox ^)sender); isBusy = false; }
Обработчик нажатия кнопки мыши MouseDown
позволить удалить объект (если нажата правая кнопка) или зафиксировать место клика для последующего перетаскивания (если нажата левая кнопка):
private: System::Void pictureBox_MouseDown (System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) { if (isBusy) return; isBusy = true; if (e->Button == System::Windows::Forms::MouseButtons::Right) { PictureBox ^ pBox = (PictureBox ^)sender; pictureBoxes->Remove(pBox); Controls->Remove(pBox); } else if (e->Button == System::Windows::Forms::MouseButtons::Left) { setActive ((PictureBox ^)sender); clickPoint = e->Location; } isBusy = false; }
При клике картинка, по которой мы щёлкнули, становится активной, за это будет отвечать метод setActive
.
Наконец, обработка перетаскивания мыши в методе MouseMove
потребует отслеживания того, не пересекается ли текущая картинка с какой-либо из других картинок списка, для этого придётся написать отдельный метод AnyBarCrossing
. Обратите также внимание, как можно получить "правильные" координаты mouseX
, mouseY
и провести их корректировку в соответствии с размером клиентской части формы.
Увы, от резких "отскакиваний" объекта при определённых обстоятельствах этот код не защитит:
private: System::Void pictureBox_MouseMove (System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) { if (activePBox!=nullptr && e->Button == System::Windows::Forms::MouseButtons::Left) { if (isBusy) return; isBusy = true; int mouseX = e->Location.X+activePBox->Location.X-clickPoint.X; if (mouseX<0) mouseX=0; if (mouseX>this->ClientSize.Width-activePBox->Width) mouseX=this->ClientSize.Width-activePBox->Width; int mouseY = e->Location.Y+activePBox->Location.Y-clickPoint.Y; if (mouseY<0) mouseY=0; if (mouseY>this->ClientSize.Height-activePBox->Height) mouseY=this->ClientSize.Height-activePBox->Height; Point point = Point(mouseX,mouseY); if (!AnyBarCrossing((PictureBox ^)sender)) { activePBox->Tag = activePBox->Location.X + "," + activePBox->Location.Y; activePBox->Location = point; } else activePBox->Location = oldPosition (); isBusy = false; } }
Обработка события PreviewKeyDown
позволит двигать активную картинку с помощью клавиш со стрелками:
private: System::Void pictureBox_PreviewKeyDown(System::Object^ sender, System::Windows::Forms::PreviewKeyDownEventArgs^ e) { if (isBusy) return; isBusy = true; if (!AnyBarCrossing((PictureBox ^)sender)) { switch (e->KeyData) { case Keys::Down: activePBox->Location = Point (activePBox->Location.X,activePBox->Location.Y+OFFSET); break; case Keys::Left: activePBox->Location = Point (activePBox->Location.X-OFFSET,activePBox->Location.Y); break; case Keys::Right: activePBox->Location = Point (activePBox->Location.X+OFFSET,activePBox->Location.Y); break; case Keys::Up: activePBox->Location = Point (activePBox->Location.X,activePBox->Location.Y-OFFSET); break; } } else activePBox->Location = oldPosition (); isBusy = false; }
Вне обработчиков событий от картинки баловаться переключением флажка isBusy
не стоит, можно получить непредсказуемые результаты.
Теперь пора писать служебные методы. Вот активация имеющейся картинки с установкой на неё фокуса. Текущее положение картинки запоминаем в свойстве Tag
, на случай если картинку придётся вернуть на место из-за недопустимого перемещения.
private: System::Void setActive(PictureBox ^pictBox) { if (activePBox != nullptr) { activePBox->BorderStyle = BorderStyle::None; } activePBox = pictBox; activePBox->BorderStyle = BorderStyle::FixedSingle; activePBox->Tag = activePBox->Location.X + "," + activePBox->Location.Y; drawXY(activePBox); activePBox->Focus(); }
Метод AnyBarCrossing
вернёт истину, если переданный ему PictureBox B
пересекается хотя бы с одним отличным от себя PictureBox A
из списка pictureBoxes
. Проверить попадание числа в диапазон ему поможет маленькая функция valueInRange
.
private: bool valueInRange(int value, int min, int max){ return (value >= min) && (value <= max); } private: bool AnyBarCrossing (PictureBox ^B) { //true, если прямоугольник B пересекается с любым другим из списка for (int i=0; i<pictureBoxes->Count; i++) { PictureBox ^ A = (PictureBox ^)pictureBoxes->default[i]; if (A!=B) { bool xOverlap = valueInRange(A->Location.X, B->Location.X, B->Location.X + B->Width) || valueInRange(B->Location.X, A->Location.X, A->Location.X + A->Width); bool yOverlap = valueInRange(A->Location.Y, B->Location.Y, B->Location.Y + B->Height) || valueInRange(B->Location.Y, A->Location.Y, A->Location.Y + A->Height); if (xOverlap && yOverlap) return true; } } return false; }
Метод drawXY
обновит статусную строку приложения (показано, как правильно это сделать), а oldPosition
вернёт предыдущую позицию картинки, которую мы заботливо сохранили в "запасном" свойстве Tag
, имеющемся у каждого контрола:
private: System::Void drawXY (PictureBox ^ pBox) { toolStripStatusLabel1->Text = String::Format("X={0}, Y={1}",pBox->Location.X, pBox->Location.Y); statusStrip1->Refresh(); } private: Point oldPosition () { if (activePBox->Tag->ToString()!=String::Empty) { String ^s = activePBox->Tag->ToString(); int i = s->IndexOf(","); if (i>-1) return Point (Int32::Parse(s->Substring(0,i)),Int32::Parse(s->Substring(i+1))); } return Point (0,0); }
В принципе, это всё, осталось написать обработчик пункта меню "Добавить" (для простоты создаём новую картинку всегда в середине формы):
PictureBox ^pictBox = AddNewPictureBox(this->ClientSize.Width/2, this->ClientSize.Height / 2); Controls->Add(pictBox); setActive(pictBox);
... и код для удаления объекта, если это действие выбрано не правой кнопкой мыши, а через меню:
if (activePBox != nullptr) { MouseEventArgs ^ e1 = gcnew MouseEventArgs(System::Windows::Forms::MouseButtons::Right,1, activePBox->Location.X,activePBox->Location.Y,0); pictureBox_MouseDown (activePBox, e1); }
Последний код также показывает, как програмнно эмулировать щелчок правой кнопкой мыши, в библиотеке .NET так вообще можно эмулировать что угодно.
Итак, меню Файл-Добавить должно добавлять новый цветной прямоугольник, их можно таскать, удалять правой кнопкой мыши, они будут "отскакивать" в угол окна при "наезде" друг на друга, но иногда будут и "залипать", так как приведённый код небезупречен, попробуйте его улучшить :) Но, главное, помните, что для каждой работы есть свой инструмент и писать чисто графически-медийные приложения средствами .NET - занятие сомнительное.
Скачать этот проект Visual C++/CLI для Studio 2010 и выше в архиве .ZIP (15 Кб)
04.11.2015, 11:04 [7091 просмотр]