БлогNot. Visual C++: преобразуем рисунок в PictureBox

Visual C++: преобразуем рисунок в PictureBox

Стандартная компонента PictureBox - удобная "рамка" для рисунка, куда его очень легко загрузить:

openFileDialog1->ShowDialog();
if (openFileDialog1->FileName != nullptr) 
 this->pictureBox1->ImageLocation = this->openFileDialog1->FileName;

(предполагается, что на форму приложения добавлен стандартный диалог открытия файла OpenFileDialog).

В дальнейшем обращаться к рисунку можно через свойство Image, а свойство SizeMode управляет режимом вывода рисунка, покрывая почти все типовые потребности:

  • значение Normal - вывод рисунка по левому верхнему углу контейнера с обрезанием;
  • значение StretchImage - вписать изображение в компонент;
  • значение AutoSize - компонент примет размеры изображения;
  • значение CenterImage - центрировать, не меняя размеры (это может обрезать рисунок);
  • значение Zoom - пропорционально масштабировать по размерам компонента (пространство имен PictureBoxSizeMode).

Не хватает, разве что, возможности стандартной прокрутки загруженного изображения, но и это легко решается: достаточно разместить PictureBox на элементе Panel с установленным свойством AutoScroll = true и при этом для PictureBox указать SizeMode = AutoSize. После этого можно загрузить рисунок кодом вида

Image ^ Img = gcnew Bitmap(openFileDialog1->FileName);
pictureBox1->Image = Img;

(здесь также предполагается, что имя файла получено из стандартного диалога открытия файлов).

Однако, у новичков часто вызывают затруднения преобразования над рисунком, загруженным в pictureBox, между тем, они также несложны, выполним примеры для основных из них.

1. Поворот и отражение изображений
Bitmap^ bitmap1 = gcnew Bitmap (pictureBox1->Image);
if ( bitmap1 != nullptr )  {
 bitmap1->RotateFlip( RotateFlipType::Rotate180FlipY );
 pictureBox1->Image = bitmap1;
}

В этом коде выведенный в настроенный как выше PictureBox рисунок отражается относительно оси 0Y и выводится обратно в компонент. Почитав о RotateFlip, легко добиться других поворотов и отражений.

2. Масштабирование изображения или его части.

Код ниже показывает, как можно программно уменьшить загруженное в компоненту PictureBox изображение в 2 раза и показать в том же pictureBox1, что получилось:

Bitmap^ bitmap1 = gcnew Bitmap (pictureBox1->Image); //взяли рисунок из компоненты
Graphics ^ Gr1 = Graphics::FromImage(bitmap1); //получили поверхность рисования из исходного рисунка
Bitmap^ bitmap2 = gcnew Bitmap (bitmap1->Width/2, bitmap1->Height/2, Gr1); 
 //сделали вдвое меньший рисунок с тем же разрешением
Graphics ^ Gr2 = Graphics::FromImage(bitmap2); //получили поверхность рисования из меньшего рисунка
Rectangle compressionRectangle = Rectangle(0, 0, bitmap1->Width/2, bitmap1->Height/2); 
 //определили масштабирующий прямоугольник
Gr2->DrawImage(bitmap1, compressionRectangle); 
 //отрисовали на поверхности второго рисунка первый со сжатием
Pen ^ MyPen = gcnew Pen(Color::Red); //на измененном рисунке можно что-то подрисовать
Gr2->DrawRectangle(MyPen, 0, 0, bitmap2->Width-1, bitmap2->Height-1); 
 //например, сделать красную рамку :)
pictureBox1->Image = bitmap2; //назначили второй рисунок компоненте
pictureBox1->Size = bitmap2->Size; //поставили размер компоненты по размерам нового рисунка
this->ClientSize = pictureBox1->Size; //...и такой же размер клиентской части формы

Изменённый рисунок можно сохранить стандартными средствами:

Bitmap ^bitmap1 = gcnew Bitmap(pictureBox1->Image);
 try {
  bitmap1->Save ("my.jpg"); 
 } 
 catch (Exception^ e) {
  MessageBox::Show(e->Message + "\nНе удалось сохранить файл", "Ошибка",MessageBoxButtons::OK, MessageBoxIcon::Exclamation);
  return;
 }

Если изменённый файл "не хочет переписываться" - применяйте решение из этой заметки.

3. Изменение цвета на изображении

На форму добавлен стандартный colorDialog и выбранный в нём цвет ставится как прозрачный.

Bitmap^ bitmap1 = gcnew Bitmap (pictureBox1->Image);
if ( colorDialog1->ShowDialog() == ::System::Windows::Forms::DialogResult::OK ) {
 bitmap1->MakeTransparent(colorDialog1->Color);
 pictureBox1->Image = bitmap1;
}
4. Фильтрация всего изображения или его части

В качестве примере уменьшим вдвое интенсивность синего цвета на загруженной в PictureBox картинке.

Bitmap^ bitmap1 = gcnew Bitmap (pictureBox1->Image);
for (int x=0; x<bitmap1->Width; x++) {
 for (int y=0; y<bitmap1->Height; y++) {
  Color pixelColor = bitmap1->GetPixel (x,y);
  Color newColor = Color::FromArgb(pixelColor.R,pixelColor.G,pixelColor.B/2);
  bitmap1->SetPixel (x,y,newColor);
 }
}
pictureBox1->Image = bitmap1;

Из-за "ручного" прохода по пикселам в двойном цикле, скорость выполнения процесса может быть заметно ниже, чем для пп. 1-2. Аналогично можно реализовать любую другую фильтрацию цветов. Более быстрый способ преобразования всех цветов рисунка даёт применение фильтрации на основе класса ColorMatrix. В качестве примера приведём код, преобразующий цветное изображение к оттенкам серого (метод ApplyGrayscaleTransformation):

private: Bitmap ^ ConvertToGrayscale (Bitmap ^ bitmap1) {
 Bitmap ^ bitmap2 = gcnew Bitmap (bitmap1->Width, bitmap1->Height);
 Graphics ^ g = Graphics::FromImage(bitmap2);
 using namespace System::Drawing::Imaging;
 array <array<float>^> ^Map = 
 {
   gcnew float[] {.3f, .3f, .3f, 0, 0},
   gcnew float[] {.59f, .59f, .59f, 0, 0},
   gcnew float[] {.11f, .11f, .11f, 0, 0},
   gcnew float[] {0, 0, 0, 1, 0},
   gcnew float[] {0, 0, 0, 0, 1}
 };
 ColorMatrix ^ colorMatrix = gcnew ColorMatrix (Map);
 ImageAttributes ^ attributes = gcnew ImageAttributes();
 attributes->SetColorMatrix(colorMatrix);
 Rectangle rect = Rectangle(0,0,bitmap1->Width,bitmap1->Height);
 g->DrawImage(bitmap1, rect, 0, 0, bitmap1->Width, bitmap1->Height, GraphicsUnit::Pixel, attributes);
 delete g;
 return bitmap2;
}

(Поправка приведённого выше кода для Studio 2015)

Этот метод можно добавить в класс формы и вызвать по нажатию кнопки или пункта меню, например, так:

Bitmap ^bitmap2 = ConvertToGrayscale (gcnew Bitmap (pictureBox1->Image));
pictureBox1->Image = bitmap2;

Подобный пример RotateColors есть и в msdn.

А вот такие, казалось бы, логичные подходы, с форматом Format16bppGrayScale работать не будут:

//Не работает с моим форматом
Bitmap^ bitmap1 = gcnew Bitmap (pictureBox1->Image);
Rectangle rect = Rectangle(0,0,bitmap1->Width,bitmap1->Height);
using namespace System::Drawing::Imaging;
PixelFormat format = PixelFormat::Format16bppGrayScale; 
 //bitmap1->PixelFormat тогда работает
Bitmap^ bitmap2 = bitmap1->Clone (rect, format);
pictureBox1->Image = bitmap2;

//Тоже не работает с моим форматом
Bitmap^ bitmap1 = gcnew Bitmap (pictureBox1->Image);
Rectangle rect = Rectangle(0,0,bitmap1->Width,bitmap1->Height);
using namespace System::Drawing::Imaging;
BitmapData ^ bmpData = bitmap1->LockBits(rect, ImageLockMode::ReadOnly, PixelFormat::Format16bppGrayScale);
Bitmap^ bitmap2 = gcnew Bitmap (bitmap1->Width, bitmap1->Height, bmpData->Stride, 
 PixelFormat::Format16bppGrayScale, bmpData->Scan0);
bitmap1->UnlockBits(bmpData);
pictureBox1->Image = bitmap2;

В инете нашёл замечание

None of the 16 bit encoders for GDI+ work. This is a long standing and well known problem

Потому и пришлось писать собственный метод для решения задачи.

5. Сохранить рисунок так, как он выглядит на компоненте

Следует понимать, что свойство SizeMode управляет отображением рисунка в компоненте, при сохранении же пропорции рисунка не изменятся от того, что он был выведен, например, при SizeMode=StretchImage (принудительно растянут по размерам компоненты, возможно, с нарушением пропорций) Тем не менее - а можно ли сохранить рисунок так, как он был выведен в компоненту? Да, можно, например, так:

Bitmap^ bitmap1 = gcnew Bitmap (pictureBox1->Image);
Bitmap ^ bitmap2 = gcnew Bitmap (pictureBox1->Width, pictureBox1->Height); //у 2-го рисунка - размер компоненты
Graphics ^ g = Graphics::FromImage(bitmap2); //получили графический контекст из 2-го рисунка
using namespace System::Drawing::Drawing2D;
g->InterpolationMode = InterpolationMode::NearestNeighbor;
g->DrawImage(bitmap1, Rectangle(0,0,bitmap2->Width,bitmap2->Height)); //Отрисовали туда исходный, неискажённый риунок
pictureBox1->Image = bitmap2; //Назначили искажённый рисунок компоненте.

Теперь сохраняем как обычно, выше показан код для этого.

6. Выделить часть рисунка и реализовать обрезку по выделенной области

В класс формы добавим следующие глобальные данные:

public:	Rectangle selRect; //выделенный прямоугольник
		Point orig; //точка для привязки прямоугольника
		Pen ^pen; //перо для отрисовки
		int flag; //флажок показывает, находимся ли в режиме выделения части рисунка

Инициализируем их, например, в имеющемся конструкторе формы:

pen = gcnew Pen(Brushes::Blue, 0.8f); //в конструкторе формы
pen->DashStyle = System::Drawing::Drawing2D::DashStyle::Dash; 
    //опишем нужное перо для отрисовки прямоугольника
selRect = Rectangle(0, 0, 0, 0);
flag = 0;
InitializeComponent();

Реализация динамического выделения мышью потребует взаимодействия нескольких событий (нажатие кнопки мыши, отпускание кнопки мыши и перемещение мыши). Для удобства используем также динамическое переключение обработчика события Paint (чтобы рисовать рамку выделения только тогда, когда она нужна – происходит перемещение курсора мыши на pictureBox при зажатой левой кнопке).

private: System::Void pictureBox1_Paint(System::Object^  sender, System::Windows::Forms::PaintEventArgs^  e) {
	//обработчик Paint для ситуации, когда выделяем рамку
	e->Graphics->DrawRectangle(Pens::Black, selRect);
}
private: System::Void Selection_Paint(System::Object^  sender, System::Windows::Forms::PaintEventArgs^  e) {
	//обработчик Paint для остальных ситуаций - добавили через События pictureBox1
	e->Graphics->DrawRectangle(pen, selRect);
}
private: System::Void pictureBox1_MouseDown(System::Object^  sender, System::Windows::Forms::MouseEventArgs^  e) {
	//Нажали мышку - включаем наш обработчик и выключаем стандартный
	pictureBox1->Paint -= gcnew PaintEventHandler(this, &Form1::pictureBox1_Paint);
	pictureBox1->Paint += gcnew PaintEventHandler(this, &Form1::Selection_Paint);
	orig = e->Location; //запомнили, где начало выделения
	flag = 1;

}
private: System::Void pictureBox1_MouseUp(System::Object^  sender, System::Windows::Forms::MouseEventArgs^  e) {
	//отжали мышку - всё наоборот
	pictureBox1->Paint -= gcnew PaintEventHandler(this, &Form1::Selection_Paint);
	pictureBox1->Paint += gcnew PaintEventHandler(this, &Form1::pictureBox1_Paint);
	pictureBox1->Invalidate(); //принудительно перерисовать
	flag = 0; //выйти из режима выделения
}

private: System::Void pictureBox1_MouseMove(System::Object^  sender, System::Windows::Forms::MouseEventArgs^  e) {
	if (flag) { //если в режиме выделения
		selRect = GetSelectionRectangle(orig, e->Location); //запоминаем выделенное
		if (e->Button == System::Windows::Forms::MouseButtons::Left) {
			pictureBox1->Refresh(); //рефрешим картинку по нажатию левой кнопки
		}
	}
}

private: Rectangle GetSelectionRectangle(Point orig, Point location) {
	//Этот метод пришлось написать, чтобы координаты выделения правильно запоминались
	//независимо от того, в какую сторону тащим курсор мыши
	Rectangle rect = Rectangle();
	int dX = location.X - orig.X, dY = location.Y - orig.Y;
	System::Drawing::Size size = System::Drawing::Size(Math::Abs(dX), Math::Abs(dY)); 
          //размеры текущего выделения
	if (dX>=0 && dY>=0) rect = Rectangle(orig,size);
	else if (dX < 0 && dY>0) rect = Rectangle(location.X, orig.Y, size.Width, size.Height);
	else if (dX>0 && dY<0) rect = Rectangle(orig.X, location.Y, size.Width, size.Height);
	else rect = Rectangle(location, size);
	return rect;
}

Теперь, при наличии на изображении выделенной рамки selRect можно, например, реализовать его обрезание по границам этой рамки:

if (selRect.Width || selRect.Height) {
		Bitmap^ bitmap1 = gcnew Bitmap(pictureBox1->Image);
		Bitmap ^ bitmap2 = gcnew Bitmap(selRect.Width, selRect.Height);
		Graphics ^ g = Graphics::FromImage(bitmap2);
		g->DrawImage(bitmap1, 0, 0, selRect, GraphicsUnit::Pixel);
		pictureBox1->Image = bitmap2;
		selRect = Rectangle(0, 0, bitmap2->Width / 2, bitmap2->Height / 2);
}

05.10.2015, 17:31 [20344 просмотра]


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

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