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