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

