Grafikus C# – Windows Forms tanfolyam › 9.7 lecke

Windows Forms · 9.7 lecke · alakzat-modell

Alakzatok rajzolása és újrarajzolása

A 9.6 leckében a vonalakat egyszerű osztályban tároltad. Egy igazi rajzoló- vagy tervezőprogramban többféle alakzat van (vonal, téglalap, ellipszis…), és mindegyiknek ugyanazt kell tudnia: kirajzolni magát, megmondani a határát, eldönteni, hogy egy pont rá esik-e, és elmozdulni. Ez az objektumorientált programozás klasszikus példája: egy közös ős (Alakzat) és leszármazottak, amelyeket egyetlen listában kezelsz (polimorfizmus). Megtanulod a rétegsorrend (z-sorrend) kezelését, a pontos találat-vizsgálatot, az átméretezést fogópontokkal, és a gyors, csak a változott részt újrarajzoló technikát.

  • kb. 130 perc
  • haladó szint
  • alakzat-modell
Diák C# programozást gyakorol egy tanulósarokban

A lecke végére

  • absztrakt ősosztályt (abstract class Alakzat) és leszármazottakat (override) írsz, és egy listában kezeled őket;
  • a rajzolás sorrendjét (z-sorrend) kezeled: előre hozás, hátra küldés;
  • pontos találat-vizsgálatot írsz vonalra, téglalapra és ellipszisre;
  • fogóponttal átméretezed az alakzatot, és kijelölést rajzolsz;
  • Invalidate(Rectangle) hívással csak a változott részt rajzoltatod újra, és a ClipRectangle alapján kihagyod a nem érintett alakzatokat;
  • felismered a tipikus hibákat (referencia-keveredés, hibás találat, szivárgó erőforrás).

Mielőtt elkezded

Ehhez a leckéhez a 9.3–9.6 lecke (Paint, Pen, Rectangle, koordináták, egéres húzás) és a 3.9 lecke (osztályok) szükséges. Az öröklődést és a polimorfizmust a 12. fejezet részletezi; itt a szükséges alapokat a példán keresztül mutatjuk meg.

9.6 MouseDown, MouseMove, MouseUp →

Az alakzat-osztályok: közös ős, saját rajz

Az abstract class olyan osztály, amelyből nem készíthetsz példányt, csak leszármazottat: azt írja elő, mit kell tudnia minden alakzatnak (abstract metódusok), a közös adatot (szín, vastagság) pedig egyszer tárolja. A leszármazottak override kulcsszóval megvalósítják a saját rajzukat. A lista List<Alakzat> típusú, és a ciklus nem tudja, melyik alakzat melyik: mindegyik a saját Rajzol metódusát hívja (polimorfizmus):

Alakzat.cs (using System.Drawing;)C#
public abstract class Alakzat
{
public Color Szin = Color.Black;
public float Vastagsag = 2;
public bool Kitoltott = false;
public bool Kijelolt = false;

public abstract void Rajzol(Graphics g); // kirajzolja magát
public abstract Rectangle Hatar(); // a befoglaló téglalap (rajz és újrarajzolás)
public abstract bool Talal(Point p); // a pont rajta van?
public abstract void Mozgat(int dx, int dy); // eltolás
public abstract Point Fogo { get; } // az átméretező fogópont helye
public abstract void FogoMozgat(Point uj); // a fogópont új helye

protected Pen UjToll()
{
return new Pen(Szin, Vastagsag);
}
}

// a három alakzat közös része: két pont (kezdő és végpont)
public abstract class KetPontos : Alakzat
{
public Point A;
public Point B;

protected Rectangle Keret()
{
return new Rectangle(Math.Min(A.X, B.X), Math.Min(A.Y, B.Y), Math.Abs(A.X - B.X), Math.Abs(A.Y - B.Y));
}

public override Rectangle Hatar()
{
Rectangle r = Keret();
int m = (int)Math.Ceiling(Vastagsag) + 2;
r.Inflate(m, m);

return r;
}

public override void Mozgat(int dx, int dy)
{
A = new Point(A.X + dx, A.Y + dy);
B = new Point(B.X + dx, B.Y + dy);
}

public override Point Fogo { get { return B; } }

public override void FogoMozgat(Point uj) { B = uj; }
}

public class Vonal : KetPontos
{
public override void Rajzol(Graphics g)
{
using (Pen toll = UjToll()) { g.DrawLine(toll, A, B); }
}

public override bool Talal(Point p)
{
return Geometria.TavolsagSzakasztol(p, A, B) <= Math.Max(4, Vastagsag / 2 + 3); // kényelmes találati sáv
}
}

public class Teglalap : KetPontos
{
public override void Rajzol(Graphics g)
{
Rectangle r = Keret();

using (Brush ecset = new SolidBrush(Color.FromArgb(70, Szin)))
using (Pen toll = UjToll())
{
if (Kitoltott) { g.FillRectangle(ecset, r); }
g.DrawRectangle(toll, r.X, r.Y, r.Width, r.Height);
}
}

public override bool Talal(Point p)
{
Rectangle r = Keret();
if (Kitoltott) { return r.Contains(p); }

int s = Math.Max(4, (int)Vastagsag / 2 + 3);
Rectangle kulso = r; kulso.Inflate(s, s);
Rectangle belso = r; belso.Inflate(-s, -s);

return kulso.Contains(p) && !belso.Contains(p); // csak a keret mentén találunk
}
}

public class Ellipszis : KetPontos
{
public override void Rajzol(Graphics g)
{
Rectangle r = Keret();

using (Brush ecset = new SolidBrush(Color.FromArgb(70, Szin)))
using (Pen toll = UjToll())
{
if (Kitoltott) { g.FillEllipse(ecset, r); }
g.DrawEllipse(toll, r);
}
}

public override bool Talal(Point p)
{
Rectangle r = Keret();
if (r.Width < 1 || r.Height < 1) { return false; }

double a = r.Width / 2.0, b = r.Height / 2.0;
double v = Math.Pow((p.X - (r.X + a)) / a, 2) + Math.Pow((p.Y - (r.Y + b)) / b, 2); // az ellipszis egyenlete: v = 1 a vonalon, < 1 belül

return Kitoltott ? v <= 1 : v >= 0.7 && v <= 1.3;
}
}

public static class Geometria
{
public static double TavolsagSzakasztol(PointF p, PointF a, PointF b)
{
double dx = b.X - a.X, dy = b.Y - a.Y, h2 = dx * dx + dy * dy;

if (h2 == 0) { return Math.Sqrt((p.X - a.X) * (p.X - a.X) + (p.Y - a.Y) * (p.Y - a.Y)); }

double t = Math.Max(0, Math.Min(1, ((p.X - a.X) * dx + (p.Y - a.Y) * dy) / h2));
double kx = a.X + t * dx, ky = a.Y + t * dy;

return Math.Sqrt((p.X - kx) * (p.X - kx) + (p.Y - ky) * (p.Y - ky));
}
}
abstract és overrideAz abstract metódusnak nincs törzse: a leszármazottnak kötelező megvalósítani (override). Ha elfelejted, a fordító jelzi. A KetPontos maga is absztrakt: közös részt (két pont, mozgatás) ad három osztálynak.
PolimorfizmusA foreach (Alakzat a in alakzatok) a.Rajzol(g); ciklus nem tudja, mi az a valódi típusa: a futás közben a megfelelő Rajzol hívódik. Új alakzat (pl. háromszög) felvételekor a ciklus nem változik.
Adat az ősbenA szín, vastagság, kitöltés minden alakzatban közös: az ősosztályban egyszer szerepel, nem háromszor.
Erőforrás-kezelésMinden alakzat a Rajzol-ban using blokkal készíti a tollat és az ecsetet, ezért nincs szivárgás (a 9.8 lecke részletezi). A UjToll() segédmetódus az ismétlődést csökkenti.

Jósolj!

Az Ellipszis osztályban elfelejted a Talal metódus megvalósítását (override). Mi történik a fordításkor?

Jóslat ellenőrzése

Fordítási hiba: az Ellipszis nem valósítja meg az absztrakt Alakzat.Talal tagot, ezért nem lehet nem absztrakt osztály. Ez az absztrakt osztály haszna: a hiányzó viselkedést a fordító jelzi, nem a futás közben derül ki.

Találat-vizsgálat és rétegsorrend (z-sorrend)

A lista sorrendje a rajzolás sorrendje: az utolsó elem van legfelül. Ezért a találatot hátulról előre keressük (a felső alakzat az, amelyiket a felhasználó lát). A z-sorrend módosítása a lista átrendezése:

Form1.csC#
private List<Alakzat> alakzatok = new List<Alakzat>();

// a legfelső alakzat az adott pontnál (vagy null)
private Alakzat TalalatKeres(Point p)
{
for (int i = alakzatok.Count - 1; i >= 0; i--)
{
if (alakzatok[i].Talal(p))
{
return alakzatok[i];
}
}

return null;
}

// z-sorrend: legelőre és legtávolabbra
private void ElreHoz(Alakzat a)
{
alakzatok.Remove(a);
alakzatok.Add(a); // a lista végére: legfelül
}

private void HatraKuld(Alakzat a)
{
alakzatok.Remove(a);
alakzatok.Insert(0, a); // a lista elejére: legalul
}

// egy lépés felfelé
private void EgyLepesFel(Alakzat a)
{
int i = alakzatok.IndexOf(a);

if (i >= 0 && i < alakzatok.Count - 1)
{
alakzatok.RemoveAt(i);
alakzatok.Insert(i + 1, a);
}
}
Hátulról előreA felső alakzat takarja az alatta lévőt, ezért a kattintás a felsőt találja el. A ciklus az első találatnál visszatér (a felső), nem keresi tovább.
Találati sávA vékony vonalat nehéz pontosan eltalálni. A tűrés (4 pixel vagy a vastagság fele + 3) kényelmessé teszi: a találat nem csak a rajzolt képponton, hanem a közelében is érvényes.
Kitöltött és üres alakzatAz üres téglalapot a belsejében nem találjuk el (átlát rajta a háttér), csak a keret mentén. A kitöltöttet a belsejében is. A Talal ezt a Kitoltott alapján dönti el.
Lista átrendezéseA z-sorrend változtatása a listából törléssel és másik helyre beszúrással megy. Ciklusban ne módosítsd a listát bejárás közben (InvalidOperationException).

Jósolj!

Két kitöltött téglalap fedi egymást; az A a lista elején (alul), a B a végén (felül). A TalalatKeres elölről (0-tól) halad. Melyiket találja el az átfedő területen kattintva?

Jóslat ellenőrzése

Az A-t (alul lévőt): az elölről kereső az első találatot adja. A felhasználó viszont a B-t látja felül, ezért a kattintás „rossz” alakzatot választ ki. A találatkeresésnek a lista végétől visszafelé kell haladnia.

Kijelölés és fogópontok

A kijelölt alakzatot keret és fogópont (kis négyzet) jelzi: erre kattintva húzással átméretezhető. Az alakzat saját Fogo tulajdonsága adja a pont helyét (itt a B végpont), a FogoMozgat az új helyét. A fogópont találatát külön vizsgáljuk, és a mozgatás előtt ellenőrizzük:

Form1.csC#
private const int FogoMeret = 8;

private Rectangle FogoTeglalap(Alakzat a)
{
Point f = a.Fogo;

return new Rectangle(f.X - FogoMeret / 2, f.Y - FogoMeret / 2, FogoMeret, FogoMeret);
}

private void KijelolesRajzol(Graphics g, Alakzat a)
{
using (Pen szagg = new Pen(Color.RoyalBlue) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dash })
{
Rectangle h = a.Hatar();
g.DrawRectangle(szagg, h.X, h.Y, h.Width, h.Height);
}

Rectangle fogo = FogoTeglalap(a);
g.FillRectangle(Brushes.White, fogo);
g.DrawRectangle(Pens.RoyalBlue, fogo.X, fogo.Y, fogo.Width, fogo.Height);
}

// MouseDown: előbb a fogópontot, utána az alakzatot vizsgáljuk
Alakzat kijelolt = alakzatok.Find(x => x.Kijelolt);

if (kijelolt != null && FogoTeglalap(kijelolt).Contains(e.Location))
{
allapot = Allapot.Atmeretez; // a fogópont húzása
}
else
{
Alakzat talalat = TalalatKeres(e.Location);
// ... kijelölés és mozgatás
}
Miért előbb a fogó?A fogópont az alakzat fölött van (a felhasználó azt látja). Ha az alakzat találatát vizsgálnád előbb, a fogópont kattintása mozgatássá válna (vagy a fogó nem lenne elérhető, ha az alakzat belsejében van).
Kényelmes méretA 8 pixeles fogó kicsi, de szokásos. Érintőképernyős programnál nagyobb kell. A fogópont találati területe lehet nagyobb a rajzoltnál.
Kijelölés jelzéseA kijelölést keret és fogópont jelzi, nem csak szín (6.9 lecke): színtévesztő felhasználó is látja. A kijelölési keret csak a Paint-ben látszik, nem része az alakzatnak.
Egy kijelöltItt egyszerre egy alakzat van kijelölve (Find); többes kijelölésnél a fogó a csoport kerete (haladó).

Jósolj!

A téglalap B pontja (a fogópont) az alakzat jobb alsó sarka. A felhasználó a fogópontot a téglalap bal felső sarka mögé (balra és felfelé) húzza. Mi történik a téglalappal?

Jóslat ellenőrzése

A téglalap átfordul: a Keret() a Min és Abs alapján mindig érvényes téglalapot ad (a két pont bármilyen sorrendben megfelel), ezért az A és a B szerepe felcserélődik, és a fogópont (B) továbbra is az egér alatt marad.

Hatékony újrarajzolás: csak a változott rész

Sok alakzatnál a teljes Invalidate() minden egérmozdulatra lassú. Két technika segít: (1) az Invalidate(Rectangle) csak a régi és az új helyet érvényteleníti, (2) a Paint az e.ClipRectangle alapján kihagyja azokat az alakzatokat, amelyek nem érintik az újrarajzolandó területet:

Form1.csC#
private int rajzolt = 0;   // kísérlethez: hány alakzatot rajzolt a legutóbbi Paint

private void rp_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.Clear(Color.White); // csak a vágási területet festi (a Paint ezt állítja be)

rajzolt = 0;

foreach (Alakzat a in alakzatok)
{
if (a.Hatar().IntersectsWith(e.ClipRectangle)) // kihagyjuk, ami nem érinti az újrarajzolandó részt
{
a.Rajzol(g);
rajzolt++;

if (a.Kijelolt) { KijelolesRajzol(g, a); }
}
}
}

// egy alakzat módosítása: a régi és az új hely együtt érvénytelen
private void Frissit(Rectangle regi, Alakzat a)
{
Rectangle terulet = Rectangle.Union(regi, a.Hatar());
terulet.Inflate(FogoMeret, FogoMeret); // a fogópont és a keret is beleférjen

rp.Invalidate(terulet);
}

// használat a MouseMove-ban
Rectangle regi = kijelolt.Hatar();
kijelolt.Mozgat(dx, dy);
Frissit(regi, kijelolt);
Régi és új helyAz alakzat elmozdulásakor a régi helyét is újra kell festeni (különben ott marad a nyoma), az újat pedig kirajzolni. A Rectangle.Union a kettőt egy téglalapba foglalja.
ClipRectangleA Paint-nek átadott e.ClipRectangle az érvénytelenített terület. A Graphics a rajzot erre vágja, így a téglalapon kívüli rajz nem számít: az alakzat kihagyásával gyorsul a program.
Hatar() legyen bővítettA Hatar() (vastagság és kijelölés miatt) tartalmazza a körvonalat. Ha túl szűk, a vastag vonal széle csonkolt marad az újrarajzolás után.
Mérj, mielőtt optimalizálszNéhány tucat alakzatnál a teljes újrarajzolás is gyors. A részleges újrarajzolás nagy rajznál (száz, ezer alakzat) és animációnál hasznos. Az egyszerűbb (teljes Invalidate) megoldás kevesebb hibalehetőséget jelent.

Jósolj!

A mozgatásnál csak az új helyet érvényteleníted (rp.Invalidate(a.Hatar())), a régi hely nélkül. Mit látsz a rajzfelületen?

Jóslat ellenőrzése

Az alakzat nyomot hagy maga után (a régi helyen megmarad a rajza, mert az a terület nem frissül). A régi és az új hely együttes érvénytelenítése (Union) tünteti el.

📷 Képernyőkép helye

Mit tegyél ide: az alakzat-szerkesztő: többféle alakzat, egy kijelölt a fogóponttal, és a „Rajzolt: 3 / 12” számláló.Javasolt fájlnév: winforms-9-7-alakzat-szerkeszto.webp · Alt szöveg: Az alakzat-szerkesztő program

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. Az alul lévő alakzat választódik ki

Átfedő alakzatoknál a felső helyett az alsó mozdul.

Ok és javítás

A találatkeresés elölről halad. A felső alakzat a lista végén van: a keresés hátulról előre (for i = Count - 1 ... 0) menjen.

2. Az alakzat nyomot hagy mozgatáskor

a.Mozgat(dx, dy);
rp.Invalidate(a.Hatar()); // csak az új hely
Hiba és javítás

A régi helyet is érvényteleníteni kell: Rectangle regi = a.Hatar(); a.Mozgat(...); rp.Invalidate(Rectangle.Union(regi, a.Hatar()));.

3. A vastag vonal széle levágódik

Újrarajzolás után a vonal széle csonka marad.

Ok és javítás

A Hatar() nem tartalmazza a vonalvastagságot (és a végződéseket). Bővítsd a téglalapot a vastagság felével és néhány pixellel.

4. Az új alakzat átveszi a régi tulajdonságait

Alakzat uj = alakzatok[alakzatok.Count - 1];   // ugyanaz a hivatkozás!
alakzatok.Add(uj); // két elem, egy objektum: az egyik mozgatása mindkettőt mozgatja
Hiba és javítás

Az osztály hivatkozás típus: a másolat ugyanarra az objektumra mutat. Másoláshoz külön Masol() metódus kell (új példány az adatok átmásolásával, 7.7 lecke).

5. A kitöltött téglalap alatti alakzat nem választható

Az alsó alakzat soha nem kattintható.

Ok és javítás

Ez a helyes viselkedés (a felső takarja). Az alsó eléréséhez a felhasználónak a z-sorrendet kell módosítania (hátra küld), vagy az alakzatot a szélén kell eltalálni. Érdemes a Tab billentyűvel is léptetni a kijelölést.

6. A program lassul a mozgatás során

public override void Rajzol(Graphics g)
{
g.DrawRectangle(new Pen(Szin, Vastagsag), Keret()); // felszabadítatlan toll minden újrarajzolásnál
}
Hiba és javítás

Szivárgó Pen. Használj using (Pen toll = UjToll()) { ... } blokkot (9.8 lecke). Az Invalidate(Rectangle) és a ClipRectangle ellenőrzés is csökkenti a terhelést.

Mini projekt: alakzat-szerkesztő

Készíts szerkesztőt, amely vonalat, téglalapot és ellipszist rajzol, és később is módosíthatók:

  1. Osztályok: az Alakzat, KetPontos, Vonal, Teglalap, Ellipszis és a Geometria a lecke szerint (külön fájlban). Minden alakzat a using blokkos tollal rajzol.
  2. Felület: rp (RajzPanel, Dock = Fill), eszköztár: rbVonal, rbTeglalap, rbEllipszis, rbKijelol (RadioButton); Szín..., nudVastag, chkKitolt; Előre hoz, Hátra küld, Egy lépés fel, Egy lépés le, Törlés (Delete); állapotsor: az alakzatok száma, a legutóbbi Paint által kirajzoltak száma (Rajzolt: 3 / 12).
  3. Létrehozás: a rajzoló eszköz húzása közben gumi-alakzatként látszik (az új alakzat már a listában van, a B pont követi az egeret); egy kattintás (nulla méret) alakzatot nem hagy.
  4. Kijelölés és mozgatás: a Kijelölés eszközzel hátulról előre történő találat-vizsgálat; a kijelölt alakzat kerete és fogópontja látszik; a mozgatás a különbség módszerrel; a kijelölés után a tulajdonság-mezők az alakzat értékeit mutatják, a módosításuk az alakzatot módosítja.
  5. Átméretezés: a fogópont húzása (FogoMozgat), a fogópont találata előbb, mint az alakzaté.
  6. Z-sorrend: a négy gomb a lista átrendezésével; a lista és a kép egyezik.
  7. Hatékony újrarajzolás: az Invalidate(Rectangle) a régi és új helyre (Union, a fogóval bővítve), a Paint a ClipRectangle alapján kihagy; a lblRajzolt mutatja a hatást.
  8. Megszakítás: az Esc megszakítja a létrehozást (a félkész alakzat törlődik) és az átméretezést/mozgatást (visszaáll a kezdő állapot).
  9. Teszteld: mindhárom alakzat létrehozása és átfordított húzása, a találat átfedő alakzatoknál, üres és kitöltött alakzat kattintása, vékony vonal eltalálása, fogópont, z-sorrend, törlés, Esc, a nyom nélküli mozgatás, nagy számú alakzat (a Rajzolt számláló).
Megoldás megnyitása (próbáld ki előbb önállóan!)

Az alakzat-osztályokat a lecke mutatja (Geometria is); itt a szerkesztő vezérlése áll. Az Allapot felsorolás (Nincs, Letrehoz, Mozgat, Atmeretez) az állapotgép.

Form1.cs (using System.Collections.Generic; using System.Linq; using System.Drawing.Drawing2D;)C#
private enum Allapot { Nincs, Letrehoz, Mozgat, Atmeretez }

private List<Alakzat> alakzatok = new List<Alakzat>();
private Allapot allapot = Allapot.Nincs;
private Alakzat aktiv = null;
private Point elozo, kezdoPont;
private Point regiFogo, regiA, regiB; // az Esc visszaállításához
private Color szin = Color.SteelBlue;
private int rajzolt = 0;
private const int FogoMeret = 8;

private Alakzat Kijelolt()
{
return alakzatok.Find(x => x.Kijelolt);
}

private void Kijeloles(Alakzat a)
{
foreach (Alakzat x in alakzatok) { x.Kijelolt = x == a; }

if (a != null)
{
beallit = true;
nudVastag.Value = (decimal)Math.Max(1, Math.Min(30, a.Vastagsag));
chkKitolt.Checked = a.Kitoltott;
szin = a.Szin;
beallit = false;
}

rp.Invalidate();
}

private bool beallit = false;

private Rectangle FogoTeglalap(Alakzat a)
{
Point f = a.Fogo;
return new Rectangle(f.X - FogoMeret / 2, f.Y - FogoMeret / 2, FogoMeret, FogoMeret);
}

private Alakzat TalalatKeres(Point p)
{
for (int i = alakzatok.Count - 1; i >= 0; i--)
{
if (alakzatok[i].Talal(p)) { return alakzatok[i]; }
}

return null;
}

private void Frissit(Rectangle regi, Alakzat a)
{
Rectangle t = Rectangle.Union(regi, a.Hatar());
t.Inflate(FogoMeret + 2, FogoMeret + 2);
rp.Invalidate(t);
}

private Alakzat UjAlakzat(Point p)
{
KetPontos a;

if (rbTeglalap.Checked) { a = new Teglalap(); }
else if (rbEllipszis.Checked) { a = new Ellipszis(); }
else { a = new Vonal(); }

a.A = a.B = p;
a.Szin = szin;
a.Vastagsag = (float)nudVastag.Value;
a.Kitoltott = chkKitolt.Checked;

return a;
}

private void rp_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left) { return; }

elozo = kezdoPont = e.Location;

if (!rbKijelol.Checked)
{
aktiv = UjAlakzat(e.Location);
alakzatok.Add(aktiv);
Kijeloles(null);
allapot = Allapot.Letrehoz;
return;
}

Alakzat kij = Kijelolt();

if (kij != null && FogoTeglalap(kij).Contains(e.Location))
{
aktiv = kij;
regiFogo = kij.Fogo;
allapot = Allapot.Atmeretez;
return;
}

Alakzat talalat = TalalatKeres(e.Location);

Kijeloles(talalat);

if (talalat != null)
{
aktiv = talalat;
KetPontos kp = talalat as KetPontos;

if (kp != null) { regiA = kp.A; regiB = kp.B; }

allapot = Allapot.Mozgat;
rp.Cursor = Cursors.SizeAll;
}
}

private void rp_MouseMove(object sender, MouseEventArgs e)
{
if (aktiv == null || allapot == Allapot.Nincs) { return; }

Rectangle regi = aktiv.Hatar();

switch (allapot)
{
case Allapot.Letrehoz:
case Allapot.Atmeretez:
aktiv.FogoMozgat(e.Location);
break;

case Allapot.Mozgat:
aktiv.Mozgat(e.X - elozo.X, e.Y - elozo.Y);
elozo = e.Location;
break;
}

Frissit(regi, aktiv);
}

private void rp_MouseUp(object sender, MouseEventArgs e)
{
if (allapot == Allapot.Letrehoz && aktiv != null)
{
KetPontos kp = (KetPontos)aktiv;

if (Math.Abs(kp.A.X - kp.B.X) < 3 && Math.Abs(kp.A.Y - kp.B.Y) < 3)
{
alakzatok.Remove(aktiv); // egyetlen kattintás: nincs alakzat
}
else
{
Kijeloles(aktiv);
}
}

Veg();
}

private void Veg()
{
allapot = Allapot.Nincs;
aktiv = null;
rp.Cursor = Cursors.Cross;
Allapotsor();
rp.Invalidate();
}

private void Megszakit()
{
if (aktiv != null)
{
if (allapot == Allapot.Letrehoz) { alakzatok.Remove(aktiv); }
else if (allapot == Allapot.Atmeretez) { aktiv.FogoMozgat(regiFogo); }
else if (allapot == Allapot.Mozgat) { KetPontos kp = aktiv as KetPontos; if (kp != null) { kp.A = regiA; kp.B = regiB; } }
}

Veg();
}

protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Escape && allapot != Allapot.Nincs) { Megszakit(); return true; }

if (keyData == Keys.Delete && allapot == Allapot.Nincs) { btnTorol.PerformClick(); return true; }

return base.ProcessCmdKey(ref msg, keyData);
}

private void rp_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.Clear(Color.White);

rajzolt = 0;

foreach (Alakzat a in alakzatok)
{
Rectangle h = a.Hatar();
h.Inflate(FogoMeret, FogoMeret);

if (h.IntersectsWith(e.ClipRectangle))
{
a.Rajzol(g);
rajzolt++;

if (a.Kijelolt)
{
using (Pen szagg = new Pen(Color.RoyalBlue) { DashStyle = DashStyle.Dash })
{
Rectangle k = a.Hatar();
g.DrawRectangle(szagg, k.X, k.Y, k.Width, k.Height);
}

Rectangle fogo = FogoTeglalap(a);
g.FillRectangle(Brushes.White, fogo);
g.DrawRectangle(Pens.RoyalBlue, fogo.X, fogo.Y, fogo.Width, fogo.Height);
}
}
}

tsslRajzolt.Text = "Rajzolt: " + rajzolt + " / " + alakzatok.Count;
}

private void Allapotsor()
{
tsslDb.Text = alakzatok.Count + " alakzat";
}

private void Tulajdonsag_Valtozott(object sender, EventArgs e)
{
Alakzat a = Kijelolt();

if (beallit || a == null) { return; }

a.Vastagsag = (float)nudVastag.Value;
a.Kitoltott = chkKitolt.Checked;
a.Szin = szin;
rp.Invalidate();
}

private void btnSzin_Click(object sender, EventArgs e)
{
using (ColorDialog cd = new ColorDialog { Color = szin })
{
if (cd.ShowDialog(this) == DialogResult.OK) { szin = cd.Color; Tulajdonsag_Valtozott(sender, e); }
}
}

private void btnElre_Click(object sender, EventArgs e) { Alakzat a = Kijelolt(); if (a != null) { alakzatok.Remove(a); alakzatok.Add(a); rp.Invalidate(); } }
private void btnHatra_Click(object sender, EventArgs e) { Alakzat a = Kijelolt(); if (a != null) { alakzatok.Remove(a); alakzatok.Insert(0, a); rp.Invalidate(); } }

private void btnFel_Click(object sender, EventArgs e)
{
Alakzat a = Kijelolt();
int i = a == null ? -1 : alakzatok.IndexOf(a);

if (i >= 0 && i < alakzatok.Count - 1) { alakzatok.RemoveAt(i); alakzatok.Insert(i + 1, a); rp.Invalidate(); }
}

private void btnLe_Click(object sender, EventArgs e)
{
Alakzat a = Kijelolt();
int i = a == null ? -1 : alakzatok.IndexOf(a);

if (i > 0) { alakzatok.RemoveAt(i); alakzatok.Insert(i - 1, a); rp.Invalidate(); }
}

private void btnTorol_Click(object sender, EventArgs e)
{
Alakzat a = Kijelolt();

if (a != null) { alakzatok.Remove(a); Allapotsor(); rp.Invalidate(); }
}
📷 Képernyőkép helye

Mit tegyél ide: a kész alakzat-szerkesztő több alakzattal, egy kijelölttel és a számlálókkal.Javasolt fájlnév: winforms-9-7-kesz.webp · Alt szöveg: A kész alakzat-szerkesztő

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. Új alakzat: háromszög

Vegyél fel Haromszog osztályt. Mely metódusokat kell megvalósítanod, és mi marad a ciklusokban változatlan?

Egy lehetséges megoldás

A KetPontos leszármazottjaként csak a Rajzol és a Talal kell; a háromszög a befoglaló téglalapba írt egyenlő szárú háromszög. A Paint, a találatkeresés, a mozgatás és a z-sorrend változatlan (polimorfizmus).

Haromszog.csC#
public class Haromszog : KetPontos
{
private Point[] Csucsok()
{
Rectangle r = Keret();

return new Point[] { new Point(r.X + r.Width / 2, r.Y), new Point(r.Right, r.Bottom), new Point(r.X, r.Bottom) };
}

public override void Rajzol(Graphics g)
{
using (Pen toll = UjToll())
using (Brush ecset = new SolidBrush(Color.FromArgb(70, Szin)))
{
if (Kitoltott) { g.FillPolygon(ecset, Csucsok()); }

g.DrawPolygon(toll, Csucsok());
}
}

public override bool Talal(Point p)
{
return Keret().Contains(p); // egyszerű változat: a befoglaló téglalap
}
}

2. Másolás

A Ctrl+D a kijelölt alakzatot 10 pixellel eltolva megkettőzi. Hogyan oldod meg a hivatkozás-keveredés nélkül?

Egy lehetséges megoldás

Az Alakzat osztály kapjon Masol() metódust (MemberwiseClone, 7.7 lecke): új példányt ad az adatok másolatával; az eltolás után kerül a listába.

Alakzat.cs és Form1.csC#
public Alakzat Masol(){    return (Alakzat)this.MemberwiseClone();   // a Point és Color érték típus: a másolat önálló}// Form1.csAlakzat uj = kijelolt.Masol();uj.Mozgat(10, 10);alakzatok.Add(uj);Kijeloles(uj);

3. Összes kijelölése téglalappal

A kijelölés eszköz üres helyről indított húzása téglalapot rajzol, és kijelöli a benne lévő alakzatokat. Hogyan döntöd el, hogy egy alakzat „benne van”?

Egy lehetséges megoldás

Két lehetőség: érinti (IntersectsWith) vagy teljesen benne van (keret.Contains(a.Hatar())). Rajzolóprogramokban az utóbbi a szokás (vagy Ctrl-lal az előbbi). A többes kijelöléshez a Kijelolt jelző már alakzatonként van.

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. abstract vagy virtual?

Mi a különbség az abstract és a virtual metódus között?

Megoldás megnyitása

Az abstract metódusnak nincs törzse: a leszármazottnak kötelező megvalósítani. A virtual metódusnak van alap megvalósítása, amelyet a leszármazott felülírhat (override), de nem kötelező.

2. Mi a z-sorrend?

Melyik alakzat látszik felül: a lista első vagy utolsó eleme?

Megoldás megnyitása

Az utolsó, mert az utolsóként rajzolódik, és takarja a korábbiakat.

Közepes szint

3. Ellipszis találat

Az (50, 50, 100, 60) téglalapba írt ellipszisnél a (100, 80) pont a középpont. Mekkora a v érték? Melyik pont ad pontosan 1-et?

Megoldás megnyitása

A közép (100, 80): a=50, b=30, v = 0. A (150, 80) pont (a jobb szélén): v = (50/50)² + 0 = 1.

4. Típusvizsgálat

A kijelölt alakzatról meg akarod tudni, hogy Teglalap-e (például egyedi tulajdonsághoz). Hogyan?

Megoldás megnyitása

Az is vagy as operátorral: if (a is Teglalap t) { ... }. Ha sokat kell típus szerint döntened, az gyakran azt jelzi, hogy a viselkedést az osztályba (virtuális metódusba) kellene tenni.

Kihívás szint

5. Rajz mentése és betöltése

Az alakzatok listáját szöveges fájlba mented, és visszaolvasod. Hogyan kezeled a különböző típusokat?

Megoldás megnyitása

Soronként egy alakzat, az első mező a típus neve (Vonal, Teglalap…); a betöltés switch utasítással hozza létre a megfelelő példányt. Kész megoldás a JSON szerializálás (10.6 lecke).

Form1.csC#
private string Sor(KetPontos a){    return a.GetType().Name + ";" + a.A.X + ";" + a.A.Y + ";" + a.B.X + ";" + a.B.Y + ";" + ColorTranslator.ToHtml(a.Szin) + ";" + a.Vastagsag.ToString(System.Globalization.CultureInfo.InvariantCulture) + ";" + (a.Kitoltott ? 1 : 0);}private KetPontos Letrehoz(string tipus){    switch (tipus)    {        case "Teglalap": return new Teglalap();        case "Ellipszis": return new Ellipszis();        default: return new Vonal();    }}

6. Visszavonás parancs-objektumokkal

A létrehozás, törlés és mozgatás is visszavonható. Milyen szerkezettel oldod meg?

Megoldás megnyitása

Parancs-objektumok: abstract class Parancs { Vegrehajt(); Visszavon(); }, leszármazottak (Letrehozas, Torles, Mozgatas), egy Stack<Parancs> a visszavonáshoz. Ugyanaz a polimorfizmus, mint az alakzatoknál (12. fejezet).

Gyors önellenőrzés

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

1. Mire való az abstract class?

Közös ős, amelyből nem készül példány: előírja a kötelező viselkedést, és a közös adatot egyszer tárolja.

2. Mi a polimorfizmus haszna?

Egy listában, egy ciklussal kezelheted a különböző alakzatokat; az új típus nem változtatja a ciklust.

3. Miért hátulról előre keresel találatot?

A lista vége van felül, azt látja a felhasználó.

4. Miért kell az Invalidate-nél a régi hely is?

Különben a régi helyen nyom marad az alakzatból.

5. Mire jó a ClipRectangle a Paint-ben?

Megmutatja az újrarajzolandó területet; az azt nem érintő alakzatok kihagyhatók (gyorsabb rajz).

6. Miért előbb a fogópontot vizsgálod?

A fogó az alakzat fölött van: különben a kattintás mozgatássá válna.

7. Miért veszélyes a lista elem másolata referenciaként?

Két elem ugyanarra az objektumra mutat: az egyik módosítása mindkettőt módosítja; Masol() kell.

Mit tudsz már?

  • Absztrakt ősosztályból leszármazott alakzatokat írsz, és egy listában kezeled őket.
  • Pontos találat-vizsgálatot készítesz, és a z-sorrendet kezeled.
  • Fogóponttal átméretezel, és a kijelölést jól jelzed.
  • Csak a változott részt rajzoltatod újra (Invalidate(Rectangle), ClipRectangle).
  • Elkerülöd a referencia-keveredés és az erőforrás-szivárgás hibáit.

Következő lecke: Képek és grafikus erőforrások felszabadítása

A Dispose mintája, a using, a szivárgás felismerése, a GDI-objektumok és a nagy Bitmapok kezelése.