Grafikus C# – Windows Forms tanfolyam › 9.10 lecke Windows Forms · 9.10 · záró mini projekt A 9. fejezet záró projektje egy rajzolóprogram: szabadkézi vonal, egyenes, téglalap és ellipszis; az alakzatok kijelölhetők, mozgathatók, átméretezhetők, a rétegsorrendjük módosítható; a műveletek visszavonhatók; háttérképet tölthetsz be; az eredményt PNG-ként vagy JPEG-ként exportálhatod, a szerkeszthető rajzot pedig saját formátumban mentheted. A program mindazt egyesíti, amit a fejezetben tanultál: képkezelés (9.1–9.2), Mini projekt: rajzolóprogram
Paint és Graphics (9.3), tollak és ecsetek (9.4), koordináták (9.5), egéres húzás (9.6), az alakzat-modell (9.7) és az erőforrás-kezelés (9.8).
A projekt végére
- összetett, többszörös felelősségű grafikus alkalmazást tervezel meg és építesz fel;
- az alakzat-modellt kiegészíted új típussal (szabadkézi vonal), és a közös viselkedést az ősben tartod;
- visszavonást és újra végrehajtást valósítasz meg pillanatképekkel;
- kompozit képet (háttér + alakzatok) exportálsz PNG-be és JPEG-be;
- szerkeszthető rajzot mentesz és töltesz be szöveges formátumban, hibatűréssel;
- erőforrásokat kezelsz (
using,Dispose), és a programot végigteszteled.
Mielőtt elkezded
Ehhez a projekthez a 9.1–9.9 lecke szükséges (különösen a 9.2, 9.6, 9.7 és 9.8), valamint a 7.8 lecke (OpenFileDialog, SaveFileDialog) és a 7.4 lecke (MessageBox). Új Windows Forms projektben vedd át a 9.7 lecke osztályait (Alakzat, KetPontos, Vonal, Teglalap, Ellipszis, Geometria) és a RajzPanel osztályt (9.3).
Követelmények és pontozás
| Rész | Követelmény | Pont |
|---|---|---|
| Alakzat-modell (20) | Absztrakt Alakzat ős és leszármazottak (Vonal, Teglalap, Ellipszis, Szabadkezi); a viselkedés (Rajzol, Hatar, Talal, Mozgat, Masol) polimorfizmussal, egyetlen List<Alakzat> kezeli őket. | 12 |
Pontos találat-vizsgálat (vékony vonal tűréssel, üres és kitöltött alakzat), hátulról előre; a rajzolás a Paint-ben, a modellből. | 8 | |
| Rajzolás és szerkesztés (25) | Eszközök: szabadkézi (2 pixeles ritkítás, egy kattintás pont), vonal, téglalap, ellipszis (húzás közben gumi-előnézet), kijelölés. | 10 |
| Kijelölt alakzat mozgatása (különbség módszer), átméretezése fogóponttal; szín, vastagság, kitöltés módosítása; a kijelölés keretet és fogót kap (nem csak színt). | 9 | |
| Z-sorrend: előre, hátra, egy lépés fel/le; törlés (Delete), mindent töröl (megerősítéssel). | 6 | |
| Visszavonás (10) | Ctrl+Z és Ctrl+Y (pillanatképekkel, minden módosításra); az Esc megszakítja a húzást, és visszaállítja az állapotot. | 10 |
| Képek (15) | Háttérkép betöltése zárolás nélkül, hibatűréssel (9.1 lecke), arányos illesztéssel; eltávolítható. | 7 |
| Export PNG-be és JPEG-be (minőségcsúszka, fehér háttér a JPEG-nél, 9.2 lecke): háttér + alakzatok, kijelölés nélkül, a panel méretében. | 8 | |
| Fájl (10) | A szerkeszthető rajz mentése és betöltése szöveges (.rajz) formátumban; hibás sorok jelentése, adatvesztés nélkül; mentetlen változtatás kezelése (* a címben, megerősítés). | 10 |
| Teljesítmény és erőforrás (10) | Kettős pufferelés (RajzPanel), Invalidate(Rectangle) a régi+új helyre, ClipRectangle szerinti kihagyás; minden toll, ecset, Bitmap, Graphics using blokkban vagy a FormClosed-ben felszabadítva; a régi háttérkép a csere után. | 10 |
| Minőség (10) | Szabályok egy helyen, nincs ismétlődő kód; billentyűzetes használat (Delete, Esc, Ctrl+Z, nyilakkal mozgatás), AccessibleName, állapotsor (koordináta, alakzatok száma, aktív eszköz), érthető hibaüzenetek. | 10 |
Összesen: 100 pont.
Felületterv
| Terület | Vezérlők |
|---|---|
Menü (menu) | Fájl: Új, Megnyitás (.rajz)..., Mentés, Mentés másként..., Exportálás képként..., Kilépés; Szerkesztés: Visszavonás (Ctrl+Z), Újra (Ctrl+Y), Törlés (Delete), Mindent töröl; Nézet: Háttérkép betöltése..., Háttérkép eltávolítása; Súgó → Névjegy. |
Eszköztár (bal oldali Panel, FlowLayoutPanel) | Eszközök (RadioButton Appearance = Button): Szabadkézi, Vonal, Téglalap, Ellipszis, Kijelölés; Szín... (és a szín mintája), nudVastag (1–30), chkKitolt; z-sorrend: Előre, Hátra, Fel, Le. |
Rajzfelület (Dock = Fill) | rp (RajzPanel), a helyi menüvel (Törlés, Előre, Hátra). |
| Állapotsor | tsslEszkoz, tsslPoz (egér koordinátája), tsslDb (alakzatok és kijelölt száma), tsslUzenet (Spring). |
Gondold végig!
A visszavonáshoz két megoldás van: (a) minden műveletre egy „fordított művelet” (parancs-objektum), (b) minden módosítás előtt a teljes alakzatlista pillanatképe (másolat). Melyik az egyszerűbb, és mi az ára?
Egy lehetséges válasz
A pillanatkép (b) egyszerűbb: nem kell minden művelethez külön visszafordítást írni, a visszavonás a lista visszaállítása. Az ára a memória (minden pillanatkép másolat) és az idő; néhány száz alakzatnál és tucatnyi lépésnél ez elhanyagolható. A parancs-objektum (a) memóriatakarékos és pontosabb, de sokkal több kód (12. fejezet).
Építés lépésenként
1. lépés: az alakzat-modell bővítése
Feladat: vedd át a 9.7 osztályait; egészítsd ki az Alakzat ősét a virtual bool VanFogo és a virtual Alakzat Masol() taggal; írd meg a Szabadkezi osztályt (pontlista: Rajzol, Hatar, Talal szakaszokra, Mozgat, Masol mély másolattal).
Támpont: 9.6 lecke (szabadkézi vonal), 9.7 lecke (TavolsagSzakasztol), 7.7 lecke (másolat és hivatkozás).
2. lépés: felület és a Paint
Feladat: a felületterv megépítése, RajzPanel, a Paint: háttér (ha van), az alakzatok a ClipRectangle szerinti kihagyással, a kijelölés (keret és fogó) a végén.
Támpont: 9.3 és 9.7 lecke.
3. lépés: az egéres állapotgép
Feladat: Allapot felsorolás (Nincs, Rajzol, Letrehoz, Mozgat, Atmeretez); a MouseDown, MouseMove, MouseUp a választott eszköz szerint; Invalidate(Rectangle) a régi és új helyre.
Támpont: 9.6 és 9.7 lecke.
4. lépés: tulajdonságok és z-sorrend
Feladat: szín, vastagság, kitöltés alkalmazása a kijelöltre (és az új alakzatokra); a négy z-sorrend gomb; a törlés, helyi menü, billentyűk.
Támpont: 9.4 és 9.7 lecke.
5. lépés: visszavonás és újra
Feladat: Pillanatkep() (mély másolat), Stack<List<Alakzat>> a visszavonáshoz és az újrához; a pillanatkép a módosítás előtt készül; az Esc a félbeszakított művelet pillanatképét állítja vissza.
Támpont: 7.7 lecke (másolat), 9.6 lecke (Esc).
6. lépés: háttérkép és export
Feladat: a KepBetolt (másolat, hibatűrés), arányos illesztés (Illeszt), a régi háttér felszabadítása; az exportálás kompozit Bitmapra, PNG/JPEG a minőségcsúszkával.
Támpont: 9.1 és 9.2 lecke.
7. lépés: mentés és betöltés (.rajz)
Feladat: soronként egy alakzat (típus, pontok, szín, vastagság, kitöltés); a betöltés előbb új listába olvas, hibás sorokat jelent, a mentetlen változtatás megerősítése.
Támpont: 8.9 lecke (CSV-szerű feldolgozás, adatvesztés nélküli betöltés).
8. lépés: utómunka, hozzáférhetőség, erőforrások
Feladat: AccessibleName, billentyűzetes használat, állapotsor; minden erőforrás kezelése; a tesztlista végigjátszása.
Támpont: 6.9 és 9.8 lecke.
Mintamegoldás
Előbb próbáld meg önállóan!
A mintamegoldás az ellenőrzésre való. A felület a tiéd; itt a logikát adom meg. A 9.7 leckei osztályokat (KetPontos, Vonal, Teglalap, Ellipszis, Geometria) nem ismétlem, csak a módosításokat és az új részeket.
Megoldás: az Alakzat bővítése és a Szabadkezi osztály
// Az Alakzat ősbe (9.7) kerül két új tag:
public virtual bool VanFogo { get { return true; } }
public virtual Alakzat Masol()
{
return (Alakzat)this.MemberwiseClone(); // a Point és a Color érték típus: a KetPontos másolata önálló
}
// új alakzat: szabadkézi vonal (pontlista)
public class Szabadkezi : Alakzat
{
public List<Point> Pontok = new List<Point>();
public override bool VanFogo { get { return false; } }
public override Point Fogo { get { return Pontok.Count > 0 ? Pontok[Pontok.Count - 1] : Point.Empty; } }
public override void FogoMozgat(Point uj) { } // a szabadkézi vonalnak nincs átméretező fogója
public override Alakzat Masol()
{
Szabadkezi m = (Szabadkezi)this.MemberwiseClone();
m.Pontok = new List<Point>(this.Pontok); // mély másolat: a lista nem lehet közös
return m;
}
public override Rectangle Hatar()
{
if (Pontok.Count == 0) { return Rectangle.Empty; }
int minX = Pontok.Min(p => p.X), maxX = Pontok.Max(p => p.X), minY = Pontok.Min(p => p.Y), maxY = Pontok.Max(p => p.Y);
int m = (int)Math.Ceiling(Vastagsag / 2) + 3;
return new Rectangle(minX - m, minY - m, maxX - minX + 2 * m, maxY - minY + 2 * m);
}
public override void Rajzol(Graphics g)
{
using (Pen toll = UjToll())
using (Brush b = new SolidBrush(Szin))
{
toll.StartCap = toll.EndCap = LineCap.Round;
toll.LineJoin = LineJoin.Round;
if (Pontok.Count == 1) { g.FillEllipse(b, Pontok[0].X - Vastagsag / 2, Pontok[0].Y - Vastagsag / 2, Vastagsag, Vastagsag); }
else if (Pontok.Count > 1) { g.DrawLines(toll, Pontok.ToArray()); }
}
}
public override bool Talal(Point p)
{
double tures = Math.Max(4, Vastagsag / 2 + 3);
if (Pontok.Count == 1) { return Geometria.TavolsagSzakasztol(p, Pontok[0], Pontok[0]) <= tures; }
for (int i = 1; i < Pontok.Count; i++)
{
if (Geometria.TavolsagSzakasztol(p, Pontok[i - 1], Pontok[i]) <= tures) { return true; }
}
return false;
}
public override void Mozgat(int dx, int dy)
{
for (int i = 0; i < Pontok.Count; i++) { Pontok[i] = new Point(Pontok[i].X + dx, Pontok[i].Y + dy); }
}
}Megoldás: Form1.cs (állapotgép, visszavonás, Paint)
private enum Allapot { Nincs, Rajzol, Letrehoz, Mozgat, Atmeretez }
private List<Alakzat> alakzatok = new List<Alakzat>();
private Stack<List<Alakzat>> visszavon = new Stack<List<Alakzat>>();
private Stack<List<Alakzat>> ujra = new Stack<List<Alakzat>>();
private List<Alakzat> mentettAllapot = null; // az Esc-hez: a művelet kezdete előtti állapot
private Allapot allapot = Allapot.Nincs;
private Alakzat aktiv = null;
private Point elozo;
private Color szin = Color.SteelBlue;
private Bitmap hatter = null;
private string fajl = "";
private bool modosult = false;
private bool beallit = false;
private const int FogoMeret = 8;
// ---- visszavonás pillanatképekkel ----
private List<Alakzat> Pillanatkep()
{
return alakzatok.Select(a => a.Masol()).ToList(); // mély másolat
}
private void Mentes() // a módosítás ELŐTT hívjuk
{
visszavon.Push(Pillanatkep());
ujra.Clear();
Modosult();
}
private void Visszaallit(List<Alakzat> allapotLista)
{
alakzatok = allapotLista;
Allapotsor();
rp.Invalidate();
}
private void miVisszavon_Click(object sender, EventArgs e)
{
if (visszavon.Count == 0 || allapot != Allapot.Nincs) { return; }
ujra.Push(Pillanatkep());
Visszaallit(visszavon.Pop());
}
private void miUjra_Click(object sender, EventArgs e)
{
if (ujra.Count == 0 || allapot != Allapot.Nincs) { return; }
visszavon.Push(Pillanatkep());
Visszaallit(ujra.Pop());
}
// ---- eszközök és egér ----
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;
beallit = false;
}
Allapotsor();
rp.Invalidate();
}
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)
{
Alakzat a;
if (rbSzabad.Checked) { Szabadkezi sz = new Szabadkezi(); sz.Pontok.Add(p); a = sz; }
else
{
KetPontos kp = rbTeglalap.Checked ? (KetPontos)new Teglalap() : rbEllipszis.Checked ? (KetPontos)new Ellipszis() : new Vonal();
kp.A = kp.B = p;
a = kp;
}
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 = e.Location;
mentettAllapot = Pillanatkep();
if (!rbKijelol.Checked)
{
Mentes();
aktiv = UjAlakzat(e.Location);
alakzatok.Add(aktiv);
Kijeloles(null);
allapot = rbSzabad.Checked ? Allapot.Rajzol : Allapot.Letrehoz;
return;
}
Alakzat kij = Kijelolt();
if (kij != null && kij.VanFogo && FogoTeglalap(kij).Contains(e.Location))
{
Mentes();
aktiv = kij;
allapot = Allapot.Atmeretez;
return;
}
Alakzat talalat = TalalatKeres(e.Location);
Kijeloles(talalat);
if (talalat != null)
{
Mentes();
aktiv = talalat;
allapot = Allapot.Mozgat;
rp.Cursor = Cursors.SizeAll;
}
}
private void rp_MouseMove(object sender, MouseEventArgs e)
{
tsslPoz.Text = e.X + ", " + e.Y;
if (aktiv == null || allapot == Allapot.Nincs) { return; }
Rectangle regi = aktiv.Hatar();
switch (allapot)
{
case Allapot.Rajzol:
Szabadkezi sz = (Szabadkezi)aktiv;
Point utolso = sz.Pontok[sz.Pontok.Count - 1];
if (Math.Abs(e.X - utolso.X) + Math.Abs(e.Y - utolso.Y) >= 2) { sz.Pontok.Add(e.Location); }
break;
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 is KetPontos)
{
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
visszavon.Pop(); // a felesleges pillanatkép törlése
}
}
Veg();
}
private void rp_MouseCaptureChanged(object sender, EventArgs e)
{
if (allapot != Allapot.Nincs) { Megszakit(); }
}
private void Veg()
{
allapot = Allapot.Nincs;
aktiv = null;
mentettAllapot = null;
rp.Cursor = Cursors.Cross;
Allapotsor();
rp.Invalidate();
}
private void Megszakit() // Esc: a művelet kezdete előtti állapot vissza
{
if (mentettAllapot != null)
{
alakzatok = mentettAllapot;
if (visszavon.Count > 0) { visszavon.Pop(); } // a művelet pillanatképe nem kell
}
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; }
Alakzat kij = Kijelolt();
if (kij != null && allapot == Allapot.Nincs && (keyData == Keys.Left || keyData == Keys.Right || keyData == Keys.Up || keyData == Keys.Down))
{
Mentes();
Rectangle regi = kij.Hatar();
kij.Mozgat(keyData == Keys.Left ? -5 : keyData == Keys.Right ? 5 : 0, keyData == Keys.Up ? -5 : keyData == Keys.Down ? 5 : 0);
Frissit(regi, kij);
return true;
}
return base.ProcessCmdKey(ref msg, keyData); // a Ctrl+Z / Ctrl+Y a menüpontok gyorsbillentyűi
}
// ---- tulajdonságok, z-sorrend, törlés ----
private void Tulajdonsag_Valtozott(object sender, EventArgs e)
{
Alakzat a = Kijelolt();
if (beallit || a == null) { return; }
Mentes();
a.Vastagsag = (float)nudVastag.Value;
a.Kitoltott = chkKitolt.Checked;
a.Szin = szin;
rp.Invalidate();
}
private void btnTorol_Click(object sender, EventArgs e)
{
Alakzat a = Kijelolt();
if (a != null) { Mentes(); alakzatok.Remove(a); Allapotsor(); rp.Invalidate(); }
}
private void ZSorrend(Action<Alakzat> muvelet)
{
Alakzat a = Kijelolt();
if (a != null) { Mentes(); muvelet(a); rp.Invalidate(); }
}
private void btnElre_Click(object sender, EventArgs e) { ZSorrend(a => { alakzatok.Remove(a); alakzatok.Add(a); }); }
private void btnHatra_Click(object sender, EventArgs e) { ZSorrend(a => { alakzatok.Remove(a); alakzatok.Insert(0, a); }); }
private void btnFel_Click(object sender, EventArgs e) { ZSorrend(a => { int i = alakzatok.IndexOf(a); if (i < alakzatok.Count - 1) { alakzatok.RemoveAt(i); alakzatok.Insert(i + 1, a); } }); }
private void btnLe_Click(object sender, EventArgs e) { ZSorrend(a => { int i = alakzatok.IndexOf(a); if (i > 0) { alakzatok.RemoveAt(i); alakzatok.Insert(i - 1, a); } }); }
private void miMindentTorol_Click(object sender, EventArgs e)
{
if (alakzatok.Count > 0 && MessageBox.Show(this, "Minden alakzat törlődik. (A visszavonás visszaállítja.) Folytatod?", "Mindent töröl", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) == DialogResult.Yes)
{
Mentes();
alakzatok = new List<Alakzat>();
Allapotsor();
rp.Invalidate();
}
}
// ---- rajzolás ----
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)));
}
private void KepetRajzol(Graphics g, Size meret, bool kijeloles, Rectangle klip)
{
g.SmoothingMode = SmoothingMode.AntiAlias;
g.Clear(Color.White);
if (hatter != null)
{
Size m = Illeszt(hatter.Size, meret);
g.InterpolationMode = InterpolationMode.HighQualityBilinear;
g.DrawImage(hatter, new Rectangle((meret.Width - m.Width) / 2, (meret.Height - m.Height) / 2, m.Width, m.Height));
}
foreach (Alakzat a in alakzatok)
{
Rectangle h = a.Hatar();
h.Inflate(FogoMeret, FogoMeret);
if (klip.IsEmpty || h.IntersectsWith(klip))
{
a.Rajzol(g);
if (kijeloles && 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);
}
if (a.VanFogo)
{
Rectangle fogo = FogoTeglalap(a);
g.FillRectangle(Brushes.White, fogo);
g.DrawRectangle(Pens.RoyalBlue, fogo.X, fogo.Y, fogo.Width, fogo.Height);
}
}
}
}
}
private void rp_Paint(object sender, PaintEventArgs e)
{
KepetRajzol(e.Graphics, rp.ClientSize, true, e.ClipRectangle);
}
private void Allapotsor()
{
tsslDb.Text = alakzatok.Count + " alakzat, " + alakzatok.Count(a => a.Kijelolt) + " kijelölve";
}
private void Modosult()
{
if (!modosult) { modosult = true; CimFrissit(); }
}
private void CimFrissit()
{
this.Text = (fajl.Length == 0 ? "Névtelen" : Path.GetFileName(fajl)) + (modosult ? "*" : "") + " – Rajzoló";
}Megoldás: háttérkép, export, mentés és betöltés
private void miHatter_Click(object sender, EventArgs e)
{
ofd.Filter = "Képek (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp";
if (ofd.ShowDialog(this) != DialogResult.OK) { return; }
string hiba;
Bitmap uj = KepBetolt(ofd.FileName, out hiba); // zárolás nélkül, hibatűréssel (9.1 lecke)
if (uj == null) { tsslUzenet.Text = hiba; return; }
Bitmap regi = hatter;
hatter = uj;
rp.Invalidate();
if (regi != null) { regi.Dispose(); } // az új beállítása után szabadítjuk a régit
}
private void miHatterTorol_Click(object sender, EventArgs e)
{
if (hatter != null) { hatter.Dispose(); hatter = null; rp.Invalidate(); }
}
private void miExport_Click(object sender, EventArgs e)
{
sfd.Filter = "PNG (*.png)|*.png|JPEG (*.jpg)|*.jpg";
sfd.DefaultExt = "png";
if (sfd.ShowDialog(this) != DialogResult.OK) { return; }
try
{
using (Bitmap kep = new Bitmap(rp.Width, rp.Height))
{
using (Graphics g = Graphics.FromImage(kep))
{
KepetRajzol(g, kep.Size, false, Rectangle.Empty); // kijelölés nélkül
}
if (sfd.FilterIndex == 2) { JpegMent(kep, sfd.FileName, tbMinoseg.Value); }
else { kep.Save(sfd.FileName, ImageFormat.Png); }
}
tsslUzenet.Text = "Exportálva: " + Path.GetFileName(sfd.FileName);
}
catch (Exception ex)
{
MessageBox.Show(this, "Az exportálás nem sikerült: " + ex.Message, "Exportálás", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// ---- .rajz fájl: soronként egy alakzat ----
// V;ax;ay;bx;by;#szin;vastag;kitolt (Vonal)
// T;... (Teglalap), E;... (Ellipszis)
// S;x,y x,y x,y;#szin;vastag;kitolt (Szabadkezi)
private string Sor(Alakzat a)
{
string kozos = ";" + ColorTranslator.ToHtml(a.Szin) + ";" + a.Vastagsag.ToString(CultureInfo.InvariantCulture) + ";" + (a.Kitoltott ? 1 : 0);
Szabadkezi sz = a as Szabadkezi;
if (sz != null) { return "S;" + string.Join(" ", sz.Pontok.Select(p => p.X + "," + p.Y)) + kozos; }
KetPontos kp = (KetPontos)a;
string tipus = a is Teglalap ? "T" : a is Ellipszis ? "E" : "V";
return tipus + ";" + kp.A.X + ";" + kp.A.Y + ";" + kp.B.X + ";" + kp.B.Y + kozos;
}
private Alakzat Elemez(string sor)
{
string[] m = sor.Split(';');
Alakzat a;
int kozosKezdet;
if (m[0] == "S")
{
Szabadkezi sz = new Szabadkezi();
foreach (string pont in m[1].Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))
{
string[] xy = pont.Split(',');
sz.Pontok.Add(new Point(int.Parse(xy[0]), int.Parse(xy[1])));
}
if (sz.Pontok.Count == 0) { throw new FormatException("nincs pont"); }
a = sz;
kozosKezdet = 2;
}
else
{
KetPontos kp = m[0] == "T" ? (KetPontos)new Teglalap() : m[0] == "E" ? (KetPontos)new Ellipszis() : new Vonal();
if (m[0] != "T" && m[0] != "E" && m[0] != "V") { throw new FormatException("ismeretlen típus"); }
kp.A = new Point(int.Parse(m[1]), int.Parse(m[2]));
kp.B = new Point(int.Parse(m[3]), int.Parse(m[4]));
a = kp;
kozosKezdet = 5;
}
a.Szin = ColorTranslator.FromHtml(m[kozosKezdet]);
a.Vastagsag = Math.Max(1, Math.Min(30, float.Parse(m[kozosKezdet + 1], CultureInfo.InvariantCulture)));
a.Kitoltott = m[kozosKezdet + 2] == "1";
return a;
}
private bool Ment(bool maskent)
{
string ut = fajl;
if (maskent || ut.Length == 0)
{
sfd.Filter = "Rajz (*.rajz)|*.rajz";
sfd.DefaultExt = "rajz";
if (sfd.ShowDialog(this) != DialogResult.OK) { return false; }
ut = sfd.FileName;
}
try
{
File.WriteAllLines(ut + ".tmp", alakzatok.Select(Sor), new UTF8Encoding(false));
if (File.Exists(ut)) { File.Delete(ut); }
File.Move(ut + ".tmp", ut);
fajl = ut;
modosult = false;
CimFrissit();
return true;
}
catch (Exception ex)
{
MessageBox.Show(this, "A mentés nem sikerült: " + ex.Message, "Mentés", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
}
private bool Folytathat()
{
if (!modosult) { return true; }
switch (MessageBox.Show(this, "Mented a változtatásokat?", "Rajzoló", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning))
{
case DialogResult.Yes: return Ment(false);
case DialogResult.No: return true;
default: return false;
}
}
private void miMegnyit_Click(object sender, EventArgs e)
{
ofd.Filter = "Rajz (*.rajz)|*.rajz";
if (!Folytathat() || ofd.ShowDialog(this) != DialogResult.OK) { return; }
List<Alakzat> uj = new List<Alakzat>();
int hibas = 0;
try
{
foreach (string sor in File.ReadAllLines(ofd.FileName))
{
if (sor.Trim().Length == 0) { continue; }
try { uj.Add(Elemez(sor)); }
catch (Exception) { hibas++; } // a hibás sor kimarad, számoljuk
}
}
catch (IOException ex)
{
MessageBox.Show(this, "A fájl nem olvasható: " + ex.Message, "Megnyitás", MessageBoxButtons.OK, MessageBoxIcon.Error);
return; // az eddigi rajz érintetlen
}
if (hibas > 0 && (uj.Count == 0 || MessageBox.Show(this, hibas + " hibás sor van. A többi (" + uj.Count + " alakzat) betöltsem?", "Megnyitás", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) != DialogResult.Yes))
{
return;
}
alakzatok = uj; // csak a teljes feldolgozás után
visszavon.Clear();
ujra.Clear();
fajl = ofd.FileName;
modosult = false;
CimFrissit();
Allapotsor();
rp.Invalidate();
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.UserClosing && !Folytathat()) { e.Cancel = true; }
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
if (hatter != null) { hatter.Dispose(); }
}Mit tegyél ide: a kész rajzolóprogram: háttérkép, színes alakzatok, egy kijelölt alakzat fogóponttal, az eszköztár és az állapotsor.Javasolt fájlnév: winforms-9-10-kesz.webp · Alt szöveg: A kész rajzolóprogram
Tesztlista
| # | Teszt | Várt eredmény |
|---|---|---|
| 1 | Szabadkézi vonal: lassú és gyors húzás, egy kattintás. | Sima vonal; egy kattintásra pont; nincs kivétel. |
| 2 | Vonal, téglalap, ellipszis; fordított húzás (jobbról balra, lentről felfelé). | Érvényes alakzat, gumi-előnézettel; a pár pixeles húzás nem hagy alakzatot. |
| 3 | Kijelölés: üres hely, vékony vonal, kitöltött és üres téglalap belseje. | A vékony vonal tűréssel eltalálható; az üres alakzat belseje nem találat; a kitöltött igen. |
| 4 | Átfedő alakzatok kijelölése. | A felül látszó alakzat választódik ki. |
| 5 | Mozgatás (különbség módszer), a panel szélén és azon túl. | A megfogott pont az egér alatt marad; nincs nyom a régi helyen. |
| 6 | Átméretezés fogóponttal, átfordítva; a szabadkézi vonalnak nincs fogója. | A fogó követi az egeret; a szabadkézi csak mozgatható. |
| 7 | Szín, vastagság, kitöltés módosítása a kijelölten, kijelölés nélkül. | Kijelölten az alakzat változik, egyébként az új alakzatok tulajdonsága. |
| 8 | Z-sorrend gombok. | A kép és a lista sorrendje egyezik. |
| 9 | Ctrl+Z és Ctrl+Y: létrehozás, mozgatás, törlés, z-sorrend, tulajdonság után. | Minden lépés visszavonható és újra végrehajtható; új művelet után az „újra” törlődik. |
| 10 | Esc létrehozás, mozgatás, átméretezés közben. | Az állapot a művelet előtti; nem keletkezik felesleges visszavonási lépés. |
| 11 | Húzás a panelen kívülre, az egér elengedése az ablakon kívül. | A húzás tisztán befejeződik (nem ragad be). |
| 12 | Háttérkép: PNG, JPEG, nem képfájl (.txt átnevezve), törlés a fájlból. | Arányos illesztés; érthető hibaüzenet; a fájl nem zárolt (törölhető). |
| 13 | Export PNG-be és JPEG-be (20, 85, 100 minőség). | A mentett kép a háttér és az alakzatok, kijelölés nélkül; a JPEG háttere nem fekete; a fájlméret változik. |
| 14 | Mentés .rajz fájlba, megnyitás újraindítás után. | Az alakzatok azonosak (típus, hely, szín, vastagság, kitöltés). |
| 15 | Hibás .rajz (2 elrontott sor, üres fájl, ismeretlen típus). | A hibás sorok számát jelzi; Nem esetén a rajz érintetlen. |
| 16 | Új, megnyitás, kilépés mentetlen rajzzal: Igen, Nem, Mégse. | Mentés + folytatás, elvetés + folytatás, maradás. |
| 17 | 500 alakzat, mozgatás; ablak átméretezése. | Sima mozgás (nincs villogás), a rajz teljesen újrarajzolódik átméretezéskor. |
| 18 | Billentyűzet: nyilak mozgatás, Delete; 150% betűméret; a Feladatkezelőben a memória és a GDI-objektumok száma hosszú használat után. | Minden elérhető; a memória és a GDI-szám nem nő folyamatosan. |
Gyakori hibák és önértékelés
Ezeken szoktak elcsúszni
Gondold végig, nálad előfordul-e valamelyik.
1. A visszavonás nem állítja vissza az alakzatot
A Ctrl+Z után az alakzat továbbra is a módosított állapotban van. A pillanatkép nem mély másolat (a lista elemei ugyanazok az objektumok), vagy a módosítás után készült. Készíts Ok és javítás
Masol() másolatokat, és a módosítás előtt mentsd a pillanatképet.
2. A szabadkézi vonal másolata közös pontlistát használ
Egy vonal mozgatása a másolatot is mozgatja. A Ok és javítás
MemberwiseClone sekély másolat: a List<Point> közös marad. A Szabadkezi.Masol() új listát készít (new List<Point>(Pontok)).
3. Az Esc után a visszavonás eggyel többet lép
Az Esc megszakított művelete is visszavonási lépés maradt. A pillanatkép a művelet kezdetén készül, ezért megszakításkor a verem tetejét el kell dobni (Ok és javítás
visszavon.Pop()), ahogy a Megszakit teszi.
4. A mentett JPEG háttere fekete, vagy a kijelölés is rajta van
Az exportált képen a kék keret és a fogó látszik. Az export a Ok és javítás
Paint rajzát másolja, kijelöléssel együtt. Használd a kijeloles = false paraméterű közös rajzoló metódust, és a JPEG-et fehér háttérre rajzolva mentsd (9.2 lecke).
5. A régi háttérkép a memóriában marad
Sok háttércsere után nő a memória. A régi Ok és javítás
Bitmapot a csere után Dispose-olni kell (9.8 lecke), és a FormClosed-ben az aktuálisat.
6. A betöltés elveszi a rajzot hibás fájlnál
A hibás .rajz megnyitása után a rajz üres. Új listába olvass, a hibás sorokat számold, és az aktuális rajzot csak a sikeres feldolgozás után cseréld (8.9 lecke).Ok és javítás
Az értékeléshez ellenőrizd az alábbi szempontokat is:
KepetRajzol) megy a panelre és az exportba; nincs ismétlődő kód.Bitmap, Graphics felszabadul; a visszavonási pillanatképek száma korlátozott (például 100).AccessibleName értéke érthető, a hibák szövegesek.Bővítési ötletek, ha kész vagy
Ha maradt időd, próbáld ki:
- Nagyítás és görgetés: a világ–képernyő átszámítás (9.5 lecke) és a jobb gombos mozgatás (9.6 lecke).
- Többes kijelölés: kijelölő téglalap (gumi-előnézettel) és csoportos mozgatás.
- Új alakzatok: háromszög, szöveg (
DrawString), nyíl (LineCap.ArrowAnchor). - Másolás, kivágás, beillesztés: a vágólapon a kijelölt alakzatok (szöveges formátumban) és a kép (
Clipboard.SetImage). - Átlátszóság: az alfa csatorna a színválasztóban (
tbAlfa), sakktábla háttérrel. - Rétegek: névvel ellátott rétegek, láthatóság-kapcsoló.
- Adatformátum: a
.rajzhelyett JSON (10.6 lecke).
Gyakorlótár: 6 további feladat
Három szinten gyakorolhatsz: alap, közepes és kihívás. A kész programot bővíted. Csak a próbálkozás után nyisd le a megoldást.
Alap szint
1. Új alakzat: nyíl
Vegyél fel Csak a Nyil osztályt, amely a Vonal leszármazottja, és nyílheggyel végződik. Mit kell módosítanod?Megoldás megnyitása
Rajzol metódust (a toll EndCap = LineCap.ArrowAnchor), és az eszközválasztót (Letrehoz a Nyil példányt), valamint a fájlformátumot (új típusjel). A modell, a mozgatás, a z-sorrend és a visszavonás változatlan.
2. Visszavonási lépések korlátja
A verem legfeljebb 100 lépést őrizzen. Hogyan oldod meg a A Stack típussal?Megoldás megnyitása
Stack nem tud az aljáról törölni: LinkedList<List<Alakzat>> (AddLast, RemoveFirst, RemoveLast) vagy List használható, a legrégebbi elem eldobásával.
Közepes szint
3. Csoportos mozgatás nyilakkal
A Shift + nyíl 20 pixelt mozgat, a nyíl önmagában 5-öt. Hol és hogyan döntöd el? A Megoldás megnyitása
ProcessCmdKey keyData értékében a Keys.Shift jelző benne van ((keyData & Keys.Shift) == Keys.Shift); a billentyűt előbb tisztítsd (keyData & Keys.KeyCode).int lepes = (keyData & Keys.Shift) == Keys.Shift ? 20 : 5;Keys gomb = keyData & Keys.KeyCode;if (gomb == Keys.Left) { kij.Mozgat(-lepes, 0); }
4. Mentés a vágólapra
A Másolás képként parancs a kész rajzot a vágólapra teszi. Hogyan? A Megoldás megnyitása
KepetRajzol egy Bitmapra, majd Clipboard.SetImage(kep). A vágólap a képet másolja, ezért a Bitmap utána felszabadítható (using), a gyakorlatban a vágólap viselkedése alkalmazásonként eltér: érdemes kipróbálni.
Kihívás szint
5. Parancs-minta a pillanatkép helyett
Írj Memóriatakarékos (csak a különbséget tárolja, pl. a mozgatás Parancs absztrakt osztályt (Vegrehajt, Visszavon) és leszármazottakat (LetrehozasParancs, MozgatasParancs). Mi az előnye a pillanatképpel szemben?Megoldás megnyitása
dx, dy értékét), és pontos; hátránya a több kód. A visszavonás a Stack<Parancs> tetejének Visszavon() hívása (12. fejezet).public abstract class Parancs{ public abstract void Vegrehajt(); public abstract void Visszavon();}public class MozgatasParancs : Parancs{ private readonly Alakzat alakzat; private readonly int dx, dy; public MozgatasParancs(Alakzat a, int dx, int dy) { alakzat = a; this.dx = dx; this.dy = dy; } public override void Vegrehajt() { alakzat.Mozgat(dx, dy); } public override void Visszavon() { alakzat.Mozgat(-dx, -dy); }}
6. Automatikus mentés összeomlás ellen
A program percenként egy ideiglenes fájlba menti a rajzot, és induláskor felajánlja a helyreállítást. Mit kell megoldanod? Az ideiglenes fájl az Megoldás megnyitása
ApplicationData mappában, a rendes mentés és a tiszta kilépés törli; induláskor a File.Exists alapján YesNo kérdés. A mentés try–catch védelmet kap, és nem zavarja a felhasználót (az állapotsorban jelez). Hosszabb rajznál a mentést háttérszálra kell tenni (11. fejezet).
Gyors önellenőrzés
Válaszolj fejben, és csak utána nyisd le a választ. Egyszerű: a módosítás előtt a lista mély másolata mentődik, a visszavonás ezt állítja vissza. A lista elemei hivatkozások: sekély másolatnál a pillanatkép és az élő lista ugyanazokat az objektumokat mutatja. A pillanatképnek a változtatás előtti állapotot kell tárolnia. Ugyanaz a kód két célra: nincs ismétlődés, és a mentett kép egyezik a képernyővel (kijelölés nélkül). Az új beállítása után ( Új listába olvas, a hibás sorokat számolja, és a rajzot csak a sikeres feldolgozás után cseréli. Egyetlen lista, egyetlen ciklus kezeli a különböző alakzatokat; új típusnál a ciklus nem változik.1. Miért pillanatképpel valósul meg a visszavonás?
2. Miért kell mély másolat?
3. Miért hívod a Mentés() metódust a módosítás előtt?
4. Miért van egy közös rajzoló metódus a panelnek és az exportnak?
5. Mikor szabadítod fel a régi háttérképet?
Dispose), és a Form zárásakor az aktuálisat.6. Hogyan kezeli a program a hibás .rajz fájlt?
7. Miért polimorfizmus az alakzat-lista?
Mit tudsz már? (a 9. fejezet végén)
- Képeket töltesz be, méretezel, vágsz és mentesz, és kezeled a képek erőforrásait.
- A
Painteseményben, tárolt adatokból rajzolsz, és ismered a rajzeszközöket. - Koordinátákat számolsz át, és transzformációkat használsz.
- Egéres húzást, kijelölést, mozgatást és átméretezést valósítasz meg alakzat-modellel.
- Grafikonokat rajzolsz, és hozzáférhető módon szemléltetsz adatokat.
- Elkészítettél egy teljes rajzolóprogramot visszavonással, háttérrel, exporttal és mentéssel.
Következő fejezet: fájlok, adatformátumok és beállítások
A 9. fejezet kész. A 10. fejezetben a fájlkezelés rendszerét tanuljuk: File, Path, Directory, adatfolyamok, CSV, JSON, XML és az alkalmazás beállításai.



