Grafikus C# – Windows Forms tanfolyam › 9.7 lecke Windows Forms · 9.7 lecke · alakzat-modell 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 (Alakzatok rajzolása és újrarajzolása
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.
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 aClipRectanglealapjá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.
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):
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 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.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.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:
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);
}
}Talal ezt a Kitoltott alapján dönti el.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:
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
}Paint-ben látszik, nem része az alakzatnak.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:
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);Rectangle.Union a kettőt egy téglalapba foglalja.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() (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.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.
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. 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 (Ok és javítás
for i = Count - 1 ... 0) menjen.
2. Az alakzat nyomot hagy mozgatáskor
a.Mozgat(dx, dy);
rp.Invalidate(a.Hatar()); // csak az új helyHiba é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. A Ok és javítás
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 mozgatjaHiba é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ó. 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.Ok és javítás
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:
- Osztályok: az
Alakzat,KetPontos,Vonal,Teglalap,Ellipszisés aGeometriaa lecke szerint (külön fájlban). Minden alakzat ausingblokkos tollal rajzol. - 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óbbiPaintáltal kirajzoltak száma (Rajzolt: 3 / 12). - 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
Bpont követi az egeret); egy kattintás (nulla méret) alakzatot nem hagy. - 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.
- Átméretezés: a fogópont húzása (
FogoMozgat), a fogópont találata előbb, mint az alakzaté. - Z-sorrend: a négy gomb a lista átrendezésével; a lista és a kép egyezik.
- Hatékony újrarajzolás: az
Invalidate(Rectangle)a régi és új helyre (Union, a fogóval bővítve), aPaintaClipRectanglealapján kihagy; alblRajzoltmutatja a hatást. - 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).
- 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.
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(); }
}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 A Haromszog osztályt. Mely metódusokat kell megvalósítanod, és mi marad a ciklusokban változatlan?Egy lehetséges megoldás
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).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? Az Egy lehetséges megoldás
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.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”? Két lehetőség: érinti (Egy lehetséges megoldás
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 Az abstract és a virtual metódus között?Megoldás megnyitása
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? Az utolsó, mert az utolsóként rajzolódik, és takarja a korábbiakat.Megoldás megnyitása
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 A közép (100, 80): a=50, b=30, v érték? Melyik pont ad pontosan 1-et?Megoldás megnyitása
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 Az Teglalap-e (például egyedi tulajdonsághoz). Hogyan?Megoldás megnyitása
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? Soronként egy alakzat, az első mező a típus neve (Megoldás megnyitása
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).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? Parancs-objektumok: Megoldás megnyitása
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. 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. Egy listában, egy ciklussal kezelheted a különböző alakzatokat; az új típus nem változtatja a ciklust. A lista vége van felül, azt látja a felhasználó. Különben a régi helyen nyom marad az alakzatból. Megmutatja az újrarajzolandó területet; az azt nem érintő alakzatok kihagyhatók (gyorsabb rajz). A fogó az alakzat fölött van: különben a kattintás mozgatássá válna. Két elem ugyanarra az objektumra mutat: az egyik módosítása mindkettőt módosítja; 1. Mire való az abstract class?
2. Mi a polimorfizmus haszna?
3. Miért hátulról előre keresel találatot?
4. Miért kell az Invalidate-nél a régi hely is?
5. Mire jó a ClipRectangle a Paint-ben?
6. Miért előbb a fogópontot vizsgálod?
7. Miért veszélyes a lista elem másolata referenciaként?
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.



