БлогNot. Visual C++: перетаскиваем много объектов и проверяем их пересечения

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 просмотр]


теги: графика программирование c++/cli

показать комментарии (1)