БлогNot. Visual C++/CLI: рисуем линию на PictureBox "как в Paint"

Visual C++/CLI: рисуем линию на PictureBox "как в Paint"

То есть, при движении мыши с зажатой кнопкой линия должна динамически обновляться, а при отпускании кнопки добавляться на существующий рисунок. Так как PictureBox удобно сохранять и легко снабдить полосами прокрутки, будем рисовать на ней. Компонента может быть частью интерфейса приложения как в этой заметке или растянутой на всю форму. Проект создан как приложение Windows Forms, код проверен в Visual Studio 2015.

Опишем в классе формы (после директивы #pragma endregion) необходимые данные:

private:
 Point p1, p2; //начало и конец линии
 Pen^ pen1; //перо
 Bitmap ^Img1; //основная картинка, на которой рисуем
 bool isPressed; //флажок "кнопка мыши зажата"

Для самой формы нам понадобится запрограммировать только событие Load, где мы инициализируем всё, что нужно - создадим рисунок по текущим размерам PictureBox, назначим его компоненте, создадим перо и выставим в "ложь" флажок:

private: System::Void MyForm_Load(System::Object^  sender, System::EventArgs^  e) {
 Img1 = gcnew Bitmap(pictureBox1->Width, pictureBox1->Height);
 pictureBox1->Image = Img1;
 pen1 = gcnew Pen(Color::Black);
 isPressed = false;
}

Ещё лучше было создать рисунок размером с клиентскую часть окна формы:

Img1 = gcnew Bitmap(this->ClientSize.Width, this->ClientSize.Height);

Всё остальное запрограммируем в событиях PictureBox. На нажатие кнопки мыши будем включать флажок и запоминать место клика p1:

private: System::Void pictureBox1_MouseDown(System::Object^  sender, 
 System::Windows::Forms::MouseEventArgs^  e) {
 isPressed = true;
 p1 = e->Location;
}

На отпускание кнопки получим координаты второй точки p2 и соединим её с первой, проведя линию на образе Img1. В реальном коде можно добавлять точки в какой-то контейнер, например, в список как здесь.

private: System::Void pictureBox1_MouseUp(System::Object^  sender, 
   System::Windows::Forms::MouseEventArgs^  e) {
 p2 = e->Location;
 Graphics ^gr = Graphics::FromImage(Img1);
 gr->DrawLine(pen1,p1,p2);
 gr->Save();
 isPressed = false;
 delete gr;
 pictureBox1->Invalidate();
}

На перемещение мыши обработка будет немного хитрей. Если кнопка не зажата, ничего делать не нужно, а в противном случае будем проводить текущую линию на копии рисунка Img2, созданной из Img1, чтобы не получилось "веера" из линий при перемещении мыши с зажатой кнопкой. Img2 всё равно придётся временно назначить рисунком для PictureBox, чтобы линия была видна в процессе движения мыши.

private: System::Void pictureBox1_MouseMove(System::Object^  sender, 
    System::Windows::Forms::MouseEventArgs^  e) {
 if (!isPressed) return; //Кнопка не зажата - выйти
 Point p2 = e->Location;
 Bitmap ^Img2 = gcnew Bitmap(Img1);
 pictureBox1->Image = Img2;
 Graphics ^gr = Graphics::FromImage(Img2);
 gr->DrawLine(pen1, p1, p2);
 delete gr;
 pictureBox1->Invalidate();
}

Это всё, приложение можно собирать.

P.S. Здесь у нас все координаты были "внутри PictureBox" и получались непосредственно из аргумента MouseEventArgs обработчика события. По-другому можно делать так:

форма->Location.X, форма->Location.Y
//Координаты верхнего левого угла формы

Cursor->Position.X, Cursor->Position.Y
//координаты курсора относительно экрана

int x1 = Cursor->Position.X - this->Location.X;
int y1 = Cursor->Position.Y - this->Location.Y;
//координаты мыши относительно левого верхнего угла формы - разность

К сожалению, из-за плохой "очистки мусора" (а мы создаём каждый раз временную копию рисунка при движении мыши с зажатой кнопкой) такое приложение может быстро пожирать память.

Как вариант решения проблемы, можно поддерживать список отрисованных линий и обновлять его, когда нужно, а для самой отрисовки задействовать обработчик события Paint от pictureBox1, вот вся пользовательская часть кода после #pragma endregion для той же формы.

Можно, конечно, ввести ещё список "тип фигуры" и рисовать другие объекты.

 private:
  System::Collections::Generic::List <Point>^ p1List = gcnew System::Collections::Generic::List <Point>();
  System::Collections::Generic::List <Point>^ p2List = gcnew System::Collections::Generic::List <Point>();
   //список точек, на самом деле лучше отдельный класс для рисуемых вами объектов
  Point p1, p2; //точки для текущей линии
  Pen^ pen1; //перо
  bool isPressed; //флажок "кнопка мыши зажата"
  Graphics^ gr; //графический контекст

 private: System::Void MyForm_Load(System::Object^ sender, System::EventArgs^ e) {
  pictureBox1->Image = gcnew Bitmap(this->ClientSize.Width, this->ClientSize.Height);
  gr = Graphics::FromImage (pictureBox1->Image);
  pen1 = gcnew Pen (Color::Black);
  isPressed = false;
 }
 
 private: System::Void pictureBox1_MouseDown(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) {
  isPressed = true;
  p1 = e->Location;
 }
 private: System::Void pictureBox1_MouseUp (System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) {
  p2 = e->Location;
  p1List->Add (p1);
  p2List->Add (p2);
  isPressed = false;
  pictureBox1->Invalidate(); pictureBox1->Update(); pictureBox1->Refresh(); //обновили всё "с гарантией"
  
 }
private: System::Void pictureBox1_MouseMove (System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) {
 if (!isPressed) return; //Кнопка не зажата - выйти
 pictureBox1->Invalidate();
 p2 = e->Location;
 gr->DrawLine(pen1, p1, p2);
}

private: System::Void pictureBox1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) {
 gr->Clear(DefaultBackColor);
 for (int x = 0; x < p2List->Count; x++) {
  gr->DrawLine(pen1, p1List[x], p2List[x]);
 }
}

 Скачать пример "Рисование линий как в Paint", архив .zip с проектом Visual Studio 2019 (7 Кб)

Если хотим рисовать просто карандашом "по точкам", можно применять и подход с таймером (код для той же формы):

private:
  Point p1, p2; //начало и конец линии
  Pen^ pen1; //перо
  Bitmap^ Img1; //основная картинка, на которой рисуем
  bool isPressed; //флажок "кнопка мыши зажата"
  Timer ^timer1; //таймер для перерисовки

 private: System::Void MyForm_Load(System::Object^ sender, System::EventArgs^ e) {
  Img1 = gcnew Bitmap(this->ClientSize.Width, this->ClientSize.Height);
  pictureBox1->Image = Img1;
  pen1 = gcnew Pen(Color::Black);
  isPressed = false;
  timer1 = gcnew Timer();
  timer1->Interval= 5; //интервал перерисовки, мс
  timer1->Tick += gcnew System::EventHandler(this, &MyForm::timer1_Tick);
 }
 
 private: System::Void timer1_Tick(System::Object^ sender, System::EventArgs^ e) {
  ResetImage(e);
 }

 private: System::Void ResetImage(System::EventArgs^ e) {
  Graphics^ gr = Graphics::FromImage(Img1);
  gr->DrawLine(pen1, p1, p2);
  delete gr;
  pictureBox1->Image = Img1;
  pictureBox1->Invalidate();
  p1 = p2;
 }

 private: System::Void pictureBox1_MouseDown(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) {
  timer1->Start();
  p1 = e->Location;
 }
 private: System::Void pictureBox1_MouseUp(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) {
  timer1->Stop();
 }

 private: System::Void pictureBox1_MouseMove(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) {
  p2 = e->Location;
 }

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

14.11.2017, 13:29; рейтинг: 3907