Grafikus C# – Windows Forms tanfolyam › 9.2 lecke

Windows Forms · 9.2 lecke · képfeldolgozás

Képek igazítása, méretezése és mentése

A 9.1 leckében a PictureBox csak megjelenítésben nagyította a képet. Ebben a leckében a kép tényleges méretét változtatod meg: kisebbre (bélyegkép, feltöltésre szánt kép), arányosan, jó minőségben. Megtanulod a forgatást és tükrözést (a telefonnal készített fényképek EXIF-tájolásával együtt), a vágást, és a mentést PNG, JPEG és BMP formátumban, JPEG minőség-beállítással. Ezekkel a lépésekkel már egy kis képszerkesztőt építesz.

  • kb. 110 perc
  • közepes szint
  • méretezés és mentés
Diák C# programozást gyakorol egy tanulósarokban

A lecke végére

  • arányos új méretet számolsz (illesztés egy dobozba), és jó minőségben átméretezed a képet;
  • beállítod a rajzolás minőségét (InterpolationMode, PixelOffsetMode, ImageAttributes);
  • forgatsz és tükrözöl (RotateFlip), és kezeled a fényképek EXIF-tájolását;
  • képrészletet vágsz ki (Clone és határellenőrzés);
  • PNG-be, BMP-be és beállítható minőségű JPEG-be mentesz;
  • kerülöd a tipikus hibákat (azonos fájlba mentés, fekete JPEG-háttér, torzult arány).

Mielőtt elkezded

Ehhez a leckéhez a 9.1 lecke (Bitmap, biztonságos betöltés) és a 7.8 lecke (SaveFileDialog) szükséges. A Graphics objektumot a 9.3 lecke tárgyalja részletesen; itt csak a méretezéshez szükséges részét használjuk.

9.1 PictureBox, Image, Bitmap →

Arányos és jó minőségű átméretezés

Két lépés: (1) kiszámolod az új méretet az arány megtartásával, (2) új, kisebb Bitmapra lerajzolod az eredeti képet jó minőségű beállításokkal. Az new Bitmap(kep, new Size(w, h)) rövid, de gyengébb minőséget ad, a GetThumbnailImage pedig kifejezetten alacsony minőségű: ezeket kerüld:

Form1.cs (using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging;)C#
// 1. az új méret: a kép belefér a megadott dobozba, az arány megmarad
private Size Illeszt(Size forras, Size doboz)
{
double arany = Math.Min((double)doboz.Width / forras.Width, (double)doboz.Height / forras.Height);

return new Size(Math.Max(1, (int)Math.Round(forras.Width * arany)), Math.Max(1, (int)Math.Round(forras.Height * arany)));
}

// 2. az átméretezés jó minőségben
private Bitmap Atmeretez(Image forras, int szelesseg, int magassag)
{
Bitmap uj = new Bitmap(szelesseg, magassag, PixelFormat.Format32bppArgb);
uj.SetResolution(forras.HorizontalResolution, forras.VerticalResolution);

using (Graphics g = Graphics.FromImage(uj))
using (ImageAttributes beallitas = new ImageAttributes())
{
g.InterpolationMode = InterpolationMode.HighQualityBicubic; // a legjobb minőségű mintavétel
g.SmoothingMode = SmoothingMode.HighQuality;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.CompositingQuality = CompositingQuality.HighQuality;

beallitas.SetWrapMode(WrapMode.TileFlipXY); // megelőzi a kép szélén a halvány keretet

g.DrawImage(forras, new Rectangle(0, 0, szelesseg, magassag), 0, 0, forras.Width, forras.Height, GraphicsUnit.Pixel, beallitas);
}

return uj;
}

// használat: legfeljebb 800 × 600 (nagyítani nem érdemes)
Size ujMeret = Illeszt(kep.Size, new Size(800, 600));
if (ujMeret.Width < kep.Width)
{
Bitmap kicsi = Atmeretez(kep, ujMeret.Width, ujMeret.Height);
}
ArányAz Illeszt az oldalarányok közül a kisebbet választja: így a kép biztosan belefér, és nem torzul. A Math.Max(1, ...) megakadályozza a 0 pixeles méretet (kivétel lenne).
InterpolationModeA HighQualityBicubic kicsinyítésnél a legszebb eredményt adja (lassabb, de a méretezés ritka művelet). A NearestNeighbor pixelgrafikánál kell (kemény élek).
PixelOffsetMode és WrapModeA PixelOffsetMode.HighQuality és a TileFlipXY a kép szélén keletkező elmosódott vagy halvány vonal ellen véd: apró, de látható különbség.
NagyításKisebb képet nagyítani felesleges (életlen). A feltöltésre szánt képeknél csak kicsinyítünk: ha a kép már kisebb a doboznál, marad.

Jósolj!

A kép 4000 × 3000, a doboz 800 × 800. Mekkora az Illeszt eredménye?

Jóslat ellenőrzése

Az arányok: 800 / 4000 = 0,2 és 800 / 3000 ≈ 0,267. A kisebb (0,2) a mérvadó: 800 × 600. A szélesség éri el a határt, a magasság kisebb marad (a kép 4:3 arányú).

Forgatás, tükrözés és az EXIF-tájolás

Form1.csC#
// 90 fokos forgatások és tükrözések: a képet helyben módosítja
kep.RotateFlip(RotateFlipType.Rotate90FlipNone); // 90° jobbra
kep.RotateFlip(RotateFlipType.Rotate270FlipNone); // 90° balra
kep.RotateFlip(RotateFlipType.Rotate180FlipNone);
kep.RotateFlip(RotateFlipType.RotateNoneFlipX); // vízszintes tükrözés
kep.RotateFlip(RotateFlipType.RotateNoneFlipY); // függőleges tükrözés

pbKep.Refresh(); // a PictureBox újrarajzolja a módosított képet

// a telefonnal készített fényképek „fekvőnek” látszanak: a tájolás az EXIF adatban van
private void ExifTajolas(Bitmap kep)
{
const int TajolasAzonosito = 0x0112;

if (Array.IndexOf(kep.PropertyIdList, TajolasAzonosito) < 0)
{
return; // nincs tájolás adat
}

int ertek = kep.GetPropertyItem(TajolasAzonosito).Value[0];

switch (ertek)
{
case 3: kep.RotateFlip(RotateFlipType.Rotate180FlipNone); break;
case 6: kep.RotateFlip(RotateFlipType.Rotate90FlipNone); break;
case 8: kep.RotateFlip(RotateFlipType.Rotate270FlipNone); break;
}

kep.RemovePropertyItem(TajolasAzonosito); // különben a mentett kép újra elfordulna
}
RotateFlipA RotateFlip a képet módosítja (nem újat ad), ezért a hívás után Refresh() kell a megjelenítéshez. Az eredeti megőrzéséhez dolgozz másolaton (9.1 lecke).
EXIF-tájolásA telefonok a fényképet mindig ugyanúgy tárolják, és az EXIF adatba írják, hogyan kell elforgatni. A GDI+ ezt nem alkalmazza magától, ezért a függőlegesen tartott fénykép „fekvő” lehet. A 0x0112 az EXIF tájolás kódja (3: 180°, 6: 90° jobbra, 8: 270°).
Tájolás törléseAz alkalmazás után a tájolás adatot törölni kell, különben a mentett fájlt egy másik program ismét elforgatná. Az, hogy a másolat (new Bitmap(kep)) megtartja-e a tulajdonságokat, a használt verziótól függhet: próbáld ki a saját fényképeiddel.
Tetszőleges szögA nem 90 fokos forgatás a Graphics.RotateTransform segítségével történik (9.5 lecke), az eredmény nagyobb vászonra kerül.

Jósolj!

Függőlegesen tartott telefon fényképét nyitod meg (EXIF: 6). A program nem kezeli az EXIF adatot. Mit mutat a PictureBox?

Jóslat ellenőrzése

A képet oldalára fordítva (a tárolt tájolásban): a GDI+ az EXIF tájolást nem érvényesíti. Más képnézegetők (böngésző, fotónézegető) elforgatják, ezért a hiba a saját programodban tűnik fel.

Képrészlet kivágása

A Bitmap.Clone(Rectangle, PixelFormat) a kijelölt téglalapot új képként adja. A téglalap nem lóghat ki a képből: ellenkező esetben OutOfMemoryException kivételt dob (ugyanaz a félrevezető kivétel, mint érvénytelen képfájlnál). Ezért metszed a téglalapot a kép határával:

Form1.csC#
private Bitmap Kivag(Bitmap kep, Rectangle kert)
{
// a kép határain belülre vágjuk a kért téglalapot
Rectangle hatar = Rectangle.Intersect(kert, new Rectangle(0, 0, kep.Width, kep.Height));

if (hatar.Width < 1 || hatar.Height < 1)
{
return null; // üres terület: nincs mit vágni
}

return kep.Clone(hatar, kep.PixelFormat); // új, önálló Bitmap
}

// a másik módszer: a forrás téglalapot a DrawImage-nek adjuk (méretezéssel együtt)
Bitmap reszlet = new Bitmap(200, 200);

using (Graphics g = Graphics.FromImage(reszlet))
{
g.DrawImage(kep, new Rectangle(0, 0, 200, 200), new Rectangle(50, 50, 100, 100), GraphicsUnit.Pixel); // a 100 × 100-as részlet 200 × 200-ra nagyítva
}
Rectangle.IntersectA két téglalap közös részét adja (üres téglalapot, ha nincs közös rész). Így a határon kívüli kérés is biztonságos.
CloneA Clone a kijelölt részletet új, önálló Bitmapként adja; az eredeti ettől nem változik. Mindkettőt külön kell felszabadítani (9.8 lecke).
DrawImage forrás téglalappalA kétféle téglalapos DrawImage a forrás egy részét a cél egy részére rajzolja: egyszerre vág és méretez.
Egérrel jelölésA vágandó terület egérrel való kijelölése (9.6 és 9.7 lecke) a koordináta-átszámítással (9.5 lecke) együtt készül el. Itt számokkal adjuk meg.

Jósolj!

A kép 300 × 200, a kért téglalap new Rectangle(250, 150, 100, 100) (kilóg a jobb alsó sarkon), és a kód Rectangle.Intersect nélkül hívja a Clone metódust. Mi történik?

Jóslat ellenőrzése

OutOfMemoryException (a kért terület nagyobb, mint a kép). A Rectangle.Intersect a téglalapot 50 × 50-esre vágja (250,150 → 300,200), ezzel a vágás sikerül.

Mentés: PNG, BMP és JPEG minőséggel

Form1.csC#
// PNG és BMP: egyszerű mentés (a PNG veszteségmentes, átlátszóságot is tárol)
kep.Save(utvonal, ImageFormat.Png);
kep.Save(utvonal, ImageFormat.Bmp);

// JPEG: a minőség beállítható (1–100), a JPEG veszteséges és nem tárol átlátszóságot
private ImageCodecInfo Kodek(ImageFormat formatum)
{
foreach (ImageCodecInfo k in ImageCodecInfo.GetImageEncoders())
{
if (k.FormatID == formatum.Guid) { return k; }
}

return null;
}

private void JpegMent(Image kep, string utvonal, long minoseg)
{
// az átlátszó részek feketék lennének: előbb fehér háttérre rajzoljuk a képet
using (Bitmap hattérrel = new Bitmap(kep.Width, kep.Height, PixelFormat.Format24bppRgb))
{
using (Graphics g = Graphics.FromImage(hattérrel))
{
g.Clear(Color.White);
g.DrawImage(kep, 0, 0, kep.Width, kep.Height);
}

using (EncoderParameters beallitasok = new EncoderParameters(1))
{
beallitasok.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, minoseg);
hattérrel.Save(utvonal, Kodek(ImageFormat.Jpeg), beallitasok);
}
}
}
FormátumTömörítésÁtlátszóságMikor?
PNGveszteségmentesigenÁbrák, képernyőképek, rajzok, átlátszó háttér.
JPEGveszteséges (minőség állítható)nemFényképek (kis fájl). A minőség 75–90 a gyakori kompromisszum.
BMPnincs (nagy fájl)nemEgyszerű, tömörítetlen; ritkán ajánlott.
GIFveszteségmentes, 256 színegy átlátszó színEgyszerű animáció, kevés színű ábra.
Azonos fájlba mentésHa a képet az Image.FromFile töltötte be, és ugyanarra a fájlra mentenéd, a GDI+ általános hibát dob (ExternalException: a fájl zárolt). A másolatos betöltés (9.1 lecke) ezt megoldja; a mentéshez egyébként is érdemes új fájlnevet kérni.
Fekete JPEG-háttérÁtlátszó PNG-t JPEG-be mentve az átlátszó részek feketék lesznek. A JpegMent ezért fehér háttérre rajzol.
Minőség és méretA minőség csökkentése kisebb fájlt, de látható hibákat (kockásodás) ad. Fényképnél 80–90 általában jó, 50 alatt már zavaró.
Kiterjesztés és formátumA fájl kiterjesztése nem határozza meg a tartalmat: a Save(utvonal, ImageFormat.Png) akkor is PNG-t ír, ha a név .jpg. A SaveFileDialog.FilterIndex alapján válaszd a formátumot, és a kiterjesztést illeszd hozzá (7.8 lecke).

Jósolj!

Átlátszó hátterű PNG-t simán kep.Save("kimenet.jpg", ImageFormat.Jpeg) hívással mentesz. Milyen háttere lesz a JPEG-nek?

Jóslat ellenőrzése

Fekete: a JPEG nem tárol átlátszóságot, az átlátszó képpontok feketévé válnak (az alfa nélküli átalakításnál). Ezért kell előbb fehér (vagy más) háttérre rajzolni.

📷 Képernyőkép helye

Mit tegyél ide: a mini képszerkesztő (átméretezés, forgatás, vágás, mentés), alatta a méret- és fájlméret-adatok.Javasolt fájlnév: winforms-9-2-szerkeszto.webp · Alt szöveg: Mini képszerkesztő

Hibakeresés: te vagy a hibadetektív

Mi a baj, és hogyan javítod?

Gondold végig az okot, és csak utána nyisd le a megoldást.

1. A kicsinyített kép torzult

Bitmap kicsi = Atmeretez(kep, 800, 800);   // fix négyzet
Hiba és javítás

A méret nem az arányból számolódik. Használd az Illeszt metódust: Size m = Illeszt(kep.Size, new Size(800, 800));.

2. A kicsinyített kép szögletes, pixeles

A kicsinyítés után a vonalak recések.

Ok és javítás

Az alapértelmezett (gyenge) minőségű mintavételt használod, vagy az InterpolationMode értéke NearestNeighbor. Állítsd HighQualityBicubic értékre.

3. Általános GDI+ hiba mentéskor

kep = Image.FromFile(utvonal);
kep.Save(utvonal, ImageFormat.Png); // ugyanaz a fájl, amelyet a kép zárol
Hiba és javítás

A fájlt a betöltött kép zárolja. Töltsd be másolattal (9.1 lecke), vagy mentsd másik fájlnévre.

4. OutOfMemoryException vágáskor

A Clone kivételt dob.

Ok és javítás

A téglalap kilóg a képből (vagy a szélessége/magassága nulla vagy negatív). Metszd a kép határával (Rectangle.Intersect), és ellenőrizd a méretét.

5. A fénykép elfordítva jelenik meg

A telefonos fénykép oldalára dőlt.

Ok és javítás

Az EXIF tájolást (0x0112) a GDI+ nem érvényesíti. Olvasd ki, forgasd el a képet, és töröld a tájolás adatot.

6. A JPEG háttere fekete

Az átlátszó PNG-ből mentett JPEG fekete hátterű.

Ok és javítás

A JPEG nem tárol átlátszóságot. Rajzold fehér háttérre (g.Clear(Color.White)) mentés előtt.

Mini projekt: mini képszerkesztő

Készíts egyszerű képszerkesztőt:

  1. Felület: pbKep (Zoom), bal oldalon panel: Megnyitás..., Eredeti visszaállítása; Méretezés: nudSzeles, nudMagas (px), chkArany (Arány megtartása), Átméretezés gomb; Forgatás: ↶ 90°, ↷ 90°, Vízszintes tükrözés, Függőleges tükrözés; Vágás: nudX, nudY, nudVSz, nudVM, Kivágás; Mentés: Mentés másként..., tbMinoseg (TrackBar, 10–100, alap 85) a JPEG-hez; lblInfo.
  2. Két kép: az eredeti (a betöltött másolat) és az aktualis (a szerkesztett). A szerkesztés az aktualis másolatán dolgozik; az Eredeti visszaállítása az eredetiből új másolatot készít.
  3. Betöltés: a 9.1 lecke KepBetolt metódusa, az ExifTajolas alkalmazásával.
  4. Átméretezés: ha a chkArany be van pipálva, a szélesség módosítása a magasságot is számolja (és fordítva); a méret 1–10 000 pixel; az Atmeretez jó minőségű beállításokkal.
  5. Forgatás és tükrözés: RotateFlip az aktualis képen, frissítéssel; a vágó mezők határai a kép méretéhez igazodnak.
  6. Vágás: a Kivag metódus (határellenőrzéssel); üres terület esetén érthető üzenet.
  7. Mentés: a SaveFileDialog szűrői: PNG, JPEG, BMP; a FilterIndex szerinti formátum és kiterjesztés; JPEG esetén fehér háttér és a csúszka minősége; a mentés után az állapotsor a fájlméretet is mutatja; új fájlnév (ne írja felül a megnyitottat).
  8. Teszteld: nagy fénykép, telefonos álló fénykép (EXIF), átlátszó PNG mentése JPEG-be, arányos és nem arányos méretezés, vágás kilógó téglalappal, minőségcsúszka (fájlméret változása), az eredeti visszaállítása, a régi képek felszabadítása.
Megoldás megnyitása (próbáld ki előbb önállóan!)

Az Illeszt, Atmeretez, Kivag, ExifTajolas, JpegMent, Kodek és a 9.1 leckei KepBetolt metódusokat a lecke mutatja; itt a szerkesztő vezérlése áll.

Form1.cs (using System.IO; using System.Drawing.Imaging;)C#
private Bitmap eredeti = null;
private Bitmap aktualis = null;
private bool beallit = false; // a mezők kódból való módosításakor nem reagálunk

private void Frissit()
{
pbKep.Image = aktualis;
pbKep.Refresh();

beallit = true;
nudSzeles.Value = Math.Max(1, Math.Min(10000, aktualis.Width));
nudMagas.Value = Math.Max(1, Math.Min(10000, aktualis.Height));
nudX.Maximum = aktualis.Width - 1;
nudY.Maximum = aktualis.Height - 1;
nudVSz.Maximum = aktualis.Width;
nudVM.Maximum = aktualis.Height;
beallit = false;

lblInfo.Text = aktualis.Width + " × " + aktualis.Height + " képpont";
}

private void Csere(Bitmap uj)
{
Bitmap regi = aktualis;

aktualis = uj;
Frissit();

if (regi != null && regi != eredeti) { regi.Dispose(); } // az eredetit nem szabadítjuk
}

private void btnMegnyit_Click(object sender, EventArgs e)
{
if (ofd.ShowDialog(this) != DialogResult.OK) { return; }

string hiba;
Bitmap uj = KepBetolt(ofd.FileName, out hiba);

if (uj == null) { lblInfo.Text = hiba; return; }

ExifTajolas(uj);

Bitmap regiEredeti = eredeti;
Bitmap regiAktualis = aktualis;

eredeti = uj;
aktualis = new Bitmap(eredeti);
Frissit();

if (regiAktualis != null && regiAktualis != regiEredeti) { regiAktualis.Dispose(); }
if (regiEredeti != null) { regiEredeti.Dispose(); }
}

private void btnVissza_Click(object sender, EventArgs e)
{
if (eredeti != null) { Csere(new Bitmap(eredeti)); }
}

private void nudSzeles_ValueChanged(object sender, EventArgs e)
{
if (beallit || aktualis == null || !chkArany.Checked) { return; }

beallit = true;
nudMagas.Value = Math.Max(1, Math.Min(10000, (int)Math.Round(nudSzeles.Value * aktualis.Height / aktualis.Width)));
beallit = false;
}

private void nudMagas_ValueChanged(object sender, EventArgs e)
{
if (beallit || aktualis == null || !chkArany.Checked) { return; }

beallit = true;
nudSzeles.Value = Math.Max(1, Math.Min(10000, (int)Math.Round(nudMagas.Value * aktualis.Width / aktualis.Height)));
beallit = false;
}

private void btnAtmeretez_Click(object sender, EventArgs e)
{
if (aktualis != null) { Csere(Atmeretez(aktualis, (int)nudSzeles.Value, (int)nudMagas.Value)); }
}

private void Forgat(RotateFlipType mod)
{
if (aktualis == null) { return; }

if (aktualis == eredeti) { aktualis = new Bitmap(eredeti); } // az eredetit nem módosítjuk

aktualis.RotateFlip(mod);
Frissit();
}

private void btnBalra_Click(object sender, EventArgs e) { Forgat(RotateFlipType.Rotate270FlipNone); }
private void btnJobbra_Click(object sender, EventArgs e) { Forgat(RotateFlipType.Rotate90FlipNone); }
private void btnTukorX_Click(object sender, EventArgs e) { Forgat(RotateFlipType.RotateNoneFlipX); }
private void btnTukorY_Click(object sender, EventArgs e) { Forgat(RotateFlipType.RotateNoneFlipY); }

private void btnKivag_Click(object sender, EventArgs e)
{
if (aktualis == null) { return; }

Bitmap reszlet = Kivag(aktualis, new Rectangle((int)nudX.Value, (int)nudY.Value, (int)nudVSz.Value, (int)nudVM.Value));

if (reszlet == null) { lblInfo.Text = "A kijelölt terület üres vagy a képen kívül van."; return; }

Csere(reszlet);
}

private void btnMent_Click(object sender, EventArgs e)
{
if (aktualis == null) { return; }

sfd.Filter = "PNG (*.png)|*.png|JPEG (*.jpg)|*.jpg|BMP (*.bmp)|*.bmp";
sfd.FileName = "kep-szerkesztett";

if (sfd.ShowDialog(this) != DialogResult.OK) { return; }

try
{
switch (sfd.FilterIndex)
{
case 2: JpegMent(aktualis, sfd.FileName, tbMinoseg.Value); break;
case 3: aktualis.Save(sfd.FileName, ImageFormat.Bmp); break;
default: aktualis.Save(sfd.FileName, ImageFormat.Png); break;
}

lblInfo.Text = "Mentve: " + Path.GetFileName(sfd.FileName) + " (" + (new FileInfo(sfd.FileName).Length / 1024) + " KB)";
}
catch (Exception ex)
{
MessageBox.Show(this, "A mentés nem sikerült: " + ex.Message, "Mentés", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
if (aktualis != null && aktualis != eredeti) { aktualis.Dispose(); }
if (eredeti != null) { eredeti.Dispose(); }
}
📷 Képernyőkép helye

Mit tegyél ide: a kész mini képszerkesztő egy képpel, a bal oldali vezérlőkkel és az adatsorral.Javasolt fájlnév: winforms-9-2-kesz.webp · Alt szöveg: A kész mini képszerkesztő

Három önálló gyakorlófeladat

Egyszerre csak egyet csinálj: tervezd meg, írd meg, futtasd, ellenőrizd, és csak utána nyisd ki a megoldást.

1. Négyzetes bélyegkép

Készíts 128 × 128-as négyzetes bélyegképet: a kép középső négyzetét vágd ki, és kicsinyítsd. Hogyan számolod a vágás téglalapját?

Egy lehetséges megoldás

A rövidebb oldal a négyzet oldala; a téglalap a hosszabb oldal közepén kezdődik.

Form1.csC#
int oldal = Math.Min(kep.Width, kep.Height);
Rectangle kozep = new Rectangle((kep.Width - oldal) / 2, (kep.Height - oldal) / 2, oldal, oldal);

using (Bitmap negyzet = Kivag(kep, kozep))
{
Bitmap bely = Atmeretez(negyzet, 128, 128);
}

2. Kötegelt kicsinyítés

Egy mappa összes képét kicsinyítsd legfeljebb 1024 × 1024-re egy kicsi almappába. Mire kell figyelned?

Egy lehetséges megoldás

Minden képet külön töltsz be (zárolás nélkül), átméretezel, elmentesz, és azonnal felszabadítasz (using), különben sok kép együtt elfogyasztja a memóriát. A hibás képeket átugrod és megszámolod.

Form1.csC#
int hibas = 0;foreach (string ut in Directory.GetFiles(mappa, "*.jpg"))
{
string hiba;
using (Bitmap kep = KepBetolt(ut, out hiba))
{
if (kep == null) { hibas++; continue; }

Size m = Illeszt(kep.Size, new Size(1024, 1024));

using (Bitmap kicsi = Atmeretez(kep, Math.Min(m.Width, kep.Width), Math.Min(m.Height, kep.Height)))
{
kicsi.Save(Path.Combine(celMappa, Path.GetFileName(ut)), ImageFormat.Jpeg);
}
}
}

3. Vízjel a kép sarkába

Írj © Név feliratot a kép jobb alsó sarkába (a Graphics.DrawString segítségével). Hol számolod a helyét?

Egy lehetséges megoldás

A felirat méretét a g.MeasureString adja, a pozíció: kép mérete mínusz felirat mérete mínusz margó (9.3 és 9.5 lecke).

Form1.csC#
using (Graphics g = Graphics.FromImage(kep))
using (Font betu = new Font("Segoe UI", 14))
using (Brush ecset = new SolidBrush(Color.FromArgb(160, Color.White)))
{
SizeF m = g.MeasureString("© Név", betu);
g.DrawString("© Név", betu, ecset, kep.Width - m.Width - 10, kep.Height - m.Height - 10);
}

Gyakorlótár: 6 további feladat

Három szinten gyakorolhatsz: alap, közepes és kihívás. Csak a próbálkozás után nyisd le a megoldást.

Alap szint

1. Arányos méret

A kép 1200 × 900. Mekkora lesz, ha a szélessége 400 px, az arány megmarad?

Megoldás megnyitása

Az arány 400 / 1200 = 1/3, így a magasság 900 / 3 = 300: a méret 400 × 300.

2. Formátumválasztás

Melyik formátumba mented: (a) fénykép e-mailben; (b) átlátszó háttérű logó; (c) képernyőkép szöveggel?

Megoldás megnyitása

(a) JPEG (80–90 minőség); (b) PNG; (c) PNG (veszteségmentes, a szöveg éles marad).

Közepes szint

3. Kitöltő méretezés

A kép töltse ki a 400 × 400-as dobozt (a kilógó részt levágva, cover). Mi a számolás lényege?

Megoldás megnyitása

A nagyobb arányt választod (a Math.Max), így a kép mindkét irányban kitölti a dobozt; a közepéből vágsz. (Az Illeszt a kisebbet használja, az az ellenkezője.)

4. Forgatás tetszőleges szöggel

A kép elforgatása 30 fokkal. Mekkora vászon kell, hogy semmi ne vágódjon le?

Megoldás megnyitása

A vászon szélessége: w·|cos α| + h·|sin α|, magassága: w·|sin α| + h·|cos α| (a 9.5 leckében részletezzük a koordináta-transzformációt).

Kihívás szint

5. Célméretű fájl

A JPEG legyen legfeljebb 200 KB: keresd meg a legnagyobb minőséget, amely belefér. Milyen algoritmust használsz?

Megoldás megnyitása

Bináris keresés a minőségen (1–100): mentés memóriába (MemoryStream), méret mérése, szűkítés. Így nem kell lemezre írni a próbálkozásokat.

Form1.csC#
private long JpegMeret(Bitmap kep, long minoseg)
{
using (MemoryStream ms = new MemoryStream())
using (EncoderParameters p = new EncoderParameters(1))
{
p.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, minoseg);
kep.Save(ms, Kodek(ImageFormat.Jpeg), p);
return ms.Length;
}
}

private int LegjobbMinoseg(Bitmap kep, long maxBajt)
{
int also = 10, felso = 100, jo = 10;

while (also <= felso)
{
int kozep = (also + felso) / 2;

if (JpegMeret(kep, kozep) <= maxBajt) { jo = kozep; also = kozep + 1; }
else { felso = kozep - 1; }
}

return jo;
}

6. Képméret a memóriában

Mennyi memóriát foglal egy 4000 × 3000-es Bitmap 32 bites képpontformátumban, és miért fontos ez a méretezésnél?

Megoldás megnyitása

4000 × 3000 × 4 bájt = 48 000 000 bájt ≈ 46 MB, a fájlméretétől függetlenül (egy 2 MB-os JPEG is kitömörítve ennyi). Ezért kell a nagy képeket gyorsan kicsinyíteni és felszabadítani (9.8 lecke).

Gyors önellenőrzés

Válaszolj fejben, és csak utána nyisd le a választ.

1. Miért nem elég a PictureBox nagyítása?

Az csak megjelenítés: a kép tényleges méretét nem változtatja (a mentett fájl nagy marad).

2. Mit számol az Illeszt?

A dobozba illeszkedő, arányos új méretet (a két arány közül a kisebbet).

3. Melyik InterpolationMode a legjobb minőségű?

HighQualityBicubic.

4. Mit tesz a RotateFlip?

90 fokos forgatás és tükrözés helyben; után Refresh() kell.

5. Miért kell az EXIF-tájolás kezelése?

A telefonos fényképek tájolása az EXIF adatban van, amit a GDI+ nem érvényesít.

6. Miért kell a Clone előtt a Rectangle.Intersect?

A képből kilógó téglalap OutOfMemoryException kivételt dob.

7. Mit kell tenni JPEG mentéskor átlátszó kép esetén?

Fehér (vagy más) háttérre rajzolni, különben az átlátszó rész fekete lesz.

Mit tudsz már?

  • Arányosan és jó minőségben átméretezel egy képet.
  • Forgatsz, tükrözöl, és kezeled a fényképek EXIF-tájolását.
  • Biztonságosan vágsz (határellenőrzéssel).
  • PNG, BMP és minőség-beállított JPEG formátumba mentesz.
  • Elkerülöd a tipikus hibákat (azonos fájl, fekete háttér, torzulás).

Következő lecke: Paint esemény és Graphics objektum

Saját rajzolás a vezérlők felületére: a Paint esemény, a Graphics objektum és az első alakzatok.