Grafikus C# – Windows Forms tanfolyam › 9.6 lecke Windows Forms · 9.6 lecke · egéres rajzolás Minden rajzolóprogram, játék és tervező ugyanazt a mintát használja: lenyomod az egérgombot (MouseDown, MouseMove, MouseUp
MouseDown), mozgatod (MouseMove), majd elengeded (MouseUp). Az 5.3 leckében a külön egéreseményeket már megismerted. Most ezt a hármast egyetlen állapotgéppé kapcsolod össze: szabadkézi vonal, gumivonal (előnézet húzás közben), kijelölő téglalap és alakzat-mozgatás. Megtanulod, mi történik, ha az egér elhagyja a panelt, hogyan szakítod meg a húzást az Esc billentyűvel, és miért kell a rajz adatait tárolni, nem csak megjeleníteni.
A lecke végére
- a
MouseDown,MouseMove,MouseUpeseményeket állapotgépként (huzasjelző) kapcsolod össze; - szabadkézi vonalat tárolsz és rajzolsz (pontlista), és kezeled az egyetlen kattintást is;
- gumivonalat (húzás közbeni előnézet) és kijelölő téglalapot készítesz;
- alakzatot mozgatsz (eltolás-különbség, találat-vizsgálat);
- érted az egér rögzítését (
Capture), a határon kívüli koordinátákat, és megszakítod a húzást (Esc); - különbséget teszel kattintás és húzás között (
SystemInformation.DragSize), és kezeled a jobb gombot.
Mielőtt elkezded
Ehhez a leckéhez az 5.3 lecke (egéresemények), a 9.3 lecke (Paint, Invalidate), a 9.4 lecke (Rectangle) és a 9.5 lecke (koordináták) szükséges.
A három esemény és az állapotgép
| Esemény | Mikor? | Mit teszünk benne? |
|---|---|---|
MouseDown | Az egérgomb lenyomásakor a vezérlő fölött. | A húzás kezdete: jelző beállítása (huzas = true), a kezdőpont megjegyzése. |
MouseMove | Az egér mozgásakor (gomb nélkül is!). | Ha huzas igaz: a húzás folyamata (új pont, előnézet), és Invalidate(). |
MouseUp | A gomb elengedésekor. | A húzás vége: az eredmény véglegesítése, huzas = false. |
private bool huzas = false; // az „állapot”: éppen húzunk-e?
private Point kezdo; // a húzás kezdőpontja
private Point aktualis; // az egér aktuális helye
private void rp_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left)
{
return; // csak a bal gomb kezdeményez húzást
}
huzas = true;
kezdo = e.Location;
aktualis = e.Location;
}
private void rp_MouseMove(object sender, MouseEventArgs e)
{
if (!huzas)
{
return; // a gomb nélküli mozgás nem érdekel
}
aktualis = e.Location;
rp.Invalidate(); // a Paint rajzolja az előnézetet
}
private void rp_MouseUp(object sender, MouseEventArgs e)
{
if (!huzas || e.Button != MouseButtons.Left)
{
return;
}
huzas = false;
aktualis = e.Location;
// itt véglegesítjük az eredményt (vonal, téglalap...)
rp.Invalidate();
}MouseMove gomb nélkül is folyamatosan fut. A huzas jelző mondja meg, hogy a mozgás egy húzás része (lenyomott gombbal) vagy csak a mutató vándorlása.MouseMove és a MouseUp akkor is a vezérlőhöz érkezik, ha a mutató közben elhagyta a vezérlőt. A e.X, e.Y ilyenkor negatív vagy a méreten túli lehet.e.Button megmondja, melyik gomb váltotta ki (Left, Right, Middle); a Control.MouseButtons a pillanatnyi állapotot adja. A e.Clicks kattintások száma (2 = dupla).Control.ModifierKeys megmutatja, nyomva van-e a Shift, Ctrl, Alt: (Control.ModifierKeys & Keys.Shift) == Keys.Shift. Használatos az egyenes vonalhoz (Shift) vagy a többes kijelöléshez (Ctrl).Jósolj!
A felhasználó lenyomja a bal gombot a panelen, a gombot nyomva tartva kihúzza az egeret a panelből és az ablakon kívül engedi el. Melyik vezérlő kapja a MouseUp eseményt?
Jóslat ellenőrzése
A panel (az egér rögzítése miatt), a panelhez viszonyított, a határon kívüli koordinátával. A húzás ezért tisztán befejeződik. Ha az egér rögzítése valamiért megszakad (pl. felugró ablak), a MouseCaptureChanged esemény jelzi: ilyenkor a húzást meg kell szakítani.
Szabadkézi vonal
Egy szabadkézi vonal egy pontlista: a MouseDown új vonalat kezd az első ponttal, a MouseMove minden új pontot hozzáad, a MouseUp lezárja. A vonalakat listában tároljuk, a Paint pedig újrarajzolja őket (9.3 lecke szabálya):
private class Vonas
{
public List<Point> Pontok = new List<Point>();
public Color Szin = Color.Black;
public float Vastagsag = 3;
}
private List<Vonas> vonasok = new List<Vonas>();
private Vonas epulo = null; // az éppen húzott vonal
private void rp_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left) { return; }
epulo = new Vonas { Szin = tollSzin, Vastagsag = (float)nudVastag.Value };
epulo.Pontok.Add(e.Location);
vonasok.Add(epulo);
rp.Invalidate();
}
private void rp_MouseMove(object sender, MouseEventArgs e)
{
if (epulo == null) { return; }
Point utolso = epulo.Pontok[epulo.Pontok.Count - 1];
// csak akkor veszünk fel új pontot, ha az egér legalább 2 pixelt mozdult
if (Math.Abs(e.X - utolso.X) + Math.Abs(e.Y - utolso.Y) >= 2)
{
epulo.Pontok.Add(e.Location);
rp.Invalidate();
}
}
private void rp_MouseUp(object sender, MouseEventArgs e)
{
epulo = null; // a vonal elkészült (a listában marad)
}
private void rp_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.Clear(Color.White);
foreach (Vonas v in vonasok)
{
using (Pen toll = new Pen(v.Szin, v.Vastagsag))
{
toll.StartCap = toll.EndCap = LineCap.Round;
toll.LineJoin = LineJoin.Round;
if (v.Pontok.Count == 1)
{
// egyetlen kattintás: pont (a DrawLines legalább két pontot kér)
using (Brush b = new SolidBrush(v.Szin))
{
g.FillEllipse(b, v.Pontok[0].X - v.Vastagsag / 2, v.Pontok[0].Y - v.Vastagsag / 2, v.Vastagsag, v.Vastagsag);
}
}
else
{
g.DrawLines(toll, v.Pontok.ToArray());
}
}
}
}DrawLines egy töröttvonalként köti össze őket. Sok kicsi szakasz sima ívnek látszik. A DrawCurve még simább (spline), de lassabb és nem mindig kívánatos.DrawLines legalább két pontot kér (ArgumentException egynél). Az egy pontos vonalat pontként (kitöltött körként) rajzoljuk.Bitmapra rajzolás (9.3 lecke): a MouseMove az előző és az új pont közé húz egy vonalat (DrawLine). A közvetlen pontok rajzolása hézagos vonalat ad, ezért kell a szakasz.Jósolj!
A Paint az epulo.Pontok pontjait rajzolja, de a vonasok listához a vonalat csak a MouseUp-ban adod hozzá. Mi történik, ha a húzás közben az ablakot kicsinyíted és visszaállítod?
Jóslat ellenőrzése
Ha a Paint csak a listát rajzolja, a félkész vonal eltűnik az újrarajzoláskor (nem része a listának), és a húzás befejezése után jelenik meg. Rajzold az epulo vonalat is, vagy (mint fent) vedd fel a listába már a MouseDown-ban.
Gumivonal és kijelölő téglalap
A gumivonal (rubber band) a húzás közbeni előnézet: a kezdőpontból az aktuális pontig húzott vonal, téglalap vagy ellipszis. A véglegesítésig nem része a modellnek, csak a Paint rajzolja (a huzas jelző alapján). A MouseUp teszi a modellbe:
// két pontból téglalap (a pontok bármilyen sorrendben, 9.4 lecke)
private Rectangle PontokbolRect(Point p, Point q)
{
return new Rectangle(Math.Min(p.X, q.X), Math.Min(p.Y, q.Y), Math.Abs(p.X - q.X), Math.Abs(p.Y - q.Y));
}
// a Paint-ben, a modell rajza UTÁN: az előnézet
if (huzas)
{
Rectangle keret = PontokbolRect(kezdo, aktualis);
using (Pen szagg = new Pen(Color.RoyalBlue) { DashStyle = DashStyle.Dash })
using (Brush halvany = new SolidBrush(Color.FromArgb(40, Color.RoyalBlue)))
{
g.FillRectangle(halvany, keret);
g.DrawRectangle(szagg, keret.X, keret.Y, keret.Width, keret.Height);
}
}
// MouseUp: a kijelölő téglalap kiválasztja a vonalakat, amelyek érintik
private void KijeloltBeallit(Rectangle keret)
{
foreach (Vonas v in vonasok)
{
v.Kijelolt = keret.IntersectsWith(v.Hatar()); // a vonal befoglaló téglalapja (Hatar)
}
}MouseUp-ban történik.PontokbolRect (Min, Abs) mindig érvényes téglalapot ad.IntersectsWith dönt. Egyszerű és gyors; pontosabb vizsgálat a vonal szakaszainak metszése.kezdo–aktualis pontpár adja az előnézetet; egyenes vonalnál a Shift a vízszintes/függőleges kényszert is adhatja.Jósolj!
A kijelölő téglalapot a kódod new Rectangle(kezdo.X, kezdo.Y, aktualis.X - kezdo.X, aktualis.Y - kezdo.Y) módon számolja. Mi történik, ha a felhasználó jobbról balra húz?
Jóslat ellenőrzése
A szélesség negatív: a DrawRectangle nem rajzol (vagy hibás téglalapot ad), a FillRectangle és az IntersectsWith is hibásan működik. A Min és Abs alapú PontokbolRect mindig jó.
Alakzat mozgatása húzással
A mozgatásnál három dolog kell: (1) találat-vizsgálat (a kattintás melyik alakzatra esett), (2) az előző egérpont megjegyzése, (3) a különbség (dx, dy) hozzáadása az alakzat pontjaihoz. A különbség módszer pontosabb, mint az alakzatot az egér helyére tenni (a megfogott pont nem ugrik a sarokba):
private bool mozgat = false;
private Point elozo; // az előző egérpont
private void rp_MouseDown(object sender, MouseEventArgs e)
{
// a legfelül lévő (utoljára rajzolt) találat: visszafelé keresünk
Vonas talalat = null;
for (int i = vonasok.Count - 1; i >= 0; i--)
{
if (vonasok[i].Hatar().Contains(e.Location))
{
talalat = vonasok[i];
break;
}
}
if (talalat != null)
{
foreach (Vonas v in vonasok) { v.Kijelolt = v == talalat; }
mozgat = true;
elozo = e.Location;
rp.Cursor = Cursors.SizeAll; // a mutató jelzi: mozgatás
rp.Invalidate();
}
}
private void rp_MouseMove(object sender, MouseEventArgs e)
{
if (mozgat)
{
int dx = e.X - elozo.X;
int dy = e.Y - elozo.Y;
foreach (Vonas v in vonasok)
{
if (v.Kijelolt)
{
for (int i = 0; i < v.Pontok.Count; i++)
{
v.Pontok[i] = new Point(v.Pontok[i].X + dx, v.Pontok[i].Y + dy); // a Point érték típus: új pontot adunk vissza
}
}
}
elozo = e.Location;
rp.Invalidate();
}
}
private void rp_MouseUp(object sender, MouseEventArgs e)
{
mozgat = false;
rp.Cursor = Cursors.Default;
}MouseMove-nál az előző ponthoz képesti elmozdulást (dx, dy) adjuk hozzá, és az elozo értékét frissítjük. Így a megfogott pont az egér alatt marad.Point csak egészben cserélhető (lista[i] = new Point(...)); a lista[i].X += dx nem fordul le.vezerlo.Cursor (Cursors.Hand, SizeAll, Cross) jelzi, mit tehet a felhasználó. A MouseMove-ban találat fölött kézre váltva, a húzás végén visszaállítva barátságosabb.Jósolj!
A mozgatás nem a különbséget adja hozzá, hanem a vonal első pontját az egér helyére teszi (pontok[0] = e.Location), a többit hozzá igazítja. Mit vesz észre a felhasználó?
Jóslat ellenőrzése
A vonal a megfogáskor ugrik (az első pontja az egér alá kerül, bárhol is fogta meg a felhasználó). A különbség módszernél az alakzat pontosan ott marad az egér alatt, ahol megfogták.
Mit tegyél ide: a rajzoló néhány vonallal, a kijelölő téglalappal (szaggatott keret) és egy kijelölt, mozgatott vonallal.Javasolt fájlnév: winforms-9-6-egerrajz.webp · Alt szöveg: Egéres rajzoló kijelöléssel
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 program rajzol, pedig a gomb nincs lenyomva
private void rp_MouseMove(object sender, MouseEventArgs e)
{
pontok.Add(e.Location); // nincs ellenőrzés: gomb nélkül is
rp.Invalidate();
}Hiba és javítás
A MouseMove gomb nélkül is fut. Ellenőrizd a huzas jelzőt (vagy az e.Button értékét), és csak húzás közben rajzolj.
2. ArgumentException az egy kattintásnál
g.DrawLines(toll, v.Pontok.ToArray()); // egyetlen pontnál kivételHiba és javítás
A DrawLines legalább két pontot kér. Egy pontnál rajzolj kitöltött kört (FillEllipse), vagy ellenőrizd a Count értékét.
3. A kijelölő téglalap nem látszik fordított húzásnál
Jobbról balra húzva nincs keret. Negatív szélesség/magasság. Használd a Ok és javítás
Min és Abs alapú PontokbolRect metódust.
4. A húzás „beragad” az ablakon kívüli elengedésnél
Az egér elengedése után a program tovább rajzol. Ha a Ok és javítás
MouseUp kimarad (például egy felugró ablak elvette az egér rögzítését), a jelző igaz marad. Kezeld a MouseCaptureChanged eseményt (a húzás megszakítása), és a MouseMove-ban ellenőrizd az e.Button értékét.
5. A vonal lassan, szögletesen rajzolódik
Gyors húzásnál nagyon szögletes a vonal. Az egér események frekvenciája véges, gyors mozgásnál a pontok távol vannak. A Ok és javítás
DrawLines ráadásul szögletes. A DrawCurve simább íven köti össze, vagy a raszteres változatnál szakaszokat kell húzni az előző és az új pont között.
6. Az Esc nem szakítja meg a húzást
Az Esc nem hat a panelen. A Ok és javítás
Panel nem kap billentyűeseményt (nem fókuszálható). Kezeld a Form KeyPreview = true beállításával vagy a ProcessCmdKey felülírásával (5.4 lecke), és a húzás jelzőjét hamisra állítsd.
Mini projekt: szabadkézi rajzoló kijelöléssel és mozgatással
Készíts rajzolót, amelyben a vonalak megmaradnak, kijelölhetők és mozgathatók:
- Felület:
rp(RajzPanel,Dock = Fill,Cursor = Cross); eszköztár:rbTollésrbKijelol(RadioButton, Toll, Kijelölés/Mozgatás), Szín... (ColorDialog),nudVastag(1–30), Törlés (kijelölt), Mindent töröl, Visszavonás (Ctrl+Z), Mentés PNG-be; állapotsor: a vonalak száma, a kijelöltek száma, az egér koordinátája. - Vonas osztály:
Pontok,Szin,Vastagsag,Kijelolt,Hatar()(a pontok befoglaló téglalapja a vonal vastagságával bővítve). - Toll eszköz: szabadkézi vonal (2 pixeles ritkítás), egy kattintás pont; a vonal a
MouseDown-ban kerül a listába. - Kijelölés eszköz: kattintás egy vonalra (legfelső találat) kijelöli (és mozgatható); üres helyre húzva gumi-téglalap (halvány kék kitöltés, szaggatott keret), amely a
MouseUp-ban kijelöli az érintett vonalakat; a Ctrl hozzáad a kijelöléshez. - Mozgatás: a kijelölt vonalak a különbség módszerrel mozognak; mozgatás közben
SizeAllmutató; a mozgatás a panel határain belülre korlátozott (a vonalak nem szöknek ki teljesen). - Kijelölés jelzése: a kijelölt vonal körül szaggatott keret (a
Hatar()bővítve), nem csak színnel; az állapotsor mutatja a kijelöltek számát. - Megszakítás: az Esc megszakítja a gumi-téglalapot és a mozgatást (a mozgatás visszaáll a kezdő helyre: a mozgatás előtt a pontok másolatát megjegyzed).
- Visszavonás: Ctrl+Z az utolsó vonalat törli; Delete a kijelölteket.
- Mentés: a vonalak kirajzolása egy
Bitmapra (fehér háttér), mentés PNG-be (SaveFileDialog, 9.2 lecke). - Teszteld: gyors és lassú húzás, egy kattintás, húzás a panelen kívülre és elengedés, fordított kijelölő téglalap, átfedő vonalak (a felső választódik), mozgatás és Esc, visszavonás, mentés, kicsinyített ablak.
Megoldás megnyitása (próbáld ki előbb önállóan!)
Az Allapot felsorolás (Nincs, Rajzol, Gumi, Mozgat) egyértelművé teszi az állapotgépet: egyszerre egy húzás lehet aktív.
private class Vonas
{
public List<Point> Pontok = new List<Point>();
public Color Szin = Color.Black;
public float Vastagsag = 3;
public bool Kijelolt = false;
public Rectangle Hatar()
{
int minX = Pontok.Min(p => p.X), maxX = Pontok.Max(p => p.X);
int minY = Pontok.Min(p => p.Y), maxY = Pontok.Max(p => p.Y);
int m = (int)Math.Ceiling(Vastagsag / 2) + 2;
return new Rectangle(minX - m, minY - m, maxX - minX + 2 * m, maxY - minY + 2 * m);
}
}
private enum Allapot { Nincs, Rajzol, Gumi, Mozgat }
private List<Vonas> vonasok = new List<Vonas>();
private Allapot allapot = Allapot.Nincs;
private Vonas epulo = null;
private Point kezdo, aktualis, elozo;
private Color tollSzin = Color.Black;
private List<List<Point>> mozgatasElott = null;
private void rp_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left) { return; }
kezdo = aktualis = elozo = e.Location;
if (rbToll.Checked)
{
epulo = new Vonas { Szin = tollSzin, Vastagsag = (float)nudVastag.Value };
epulo.Pontok.Add(e.Location);
vonasok.Add(epulo);
allapot = Allapot.Rajzol;
}
else
{
Vonas talalat = null;
for (int i = vonasok.Count - 1; i >= 0; i--)
{
if (vonasok[i].Hatar().Contains(e.Location)) { talalat = vonasok[i]; break; }
}
if (talalat != null)
{
if (!talalat.Kijelolt && (Control.ModifierKeys & Keys.Control) != Keys.Control)
{
foreach (Vonas v in vonasok) { v.Kijelolt = false; }
}
talalat.Kijelolt = true;
mozgatasElott = vonasok.Where(v => v.Kijelolt).Select(v => new List<Point>(v.Pontok)).ToList();
allapot = Allapot.Mozgat;
rp.Cursor = Cursors.SizeAll;
}
else
{
if ((Control.ModifierKeys & Keys.Control) != Keys.Control)
{
foreach (Vonas v in vonasok) { v.Kijelolt = false; }
}
allapot = Allapot.Gumi;
}
}
Frissit();
}
private void rp_MouseMove(object sender, MouseEventArgs e)
{
tsslEger.Text = e.X + ", " + e.Y;
aktualis = e.Location;
switch (allapot)
{
case Allapot.Rajzol:
Point utolso = epulo.Pontok[epulo.Pontok.Count - 1];
if (Math.Abs(e.X - utolso.X) + Math.Abs(e.Y - utolso.Y) >= 2) { epulo.Pontok.Add(e.Location); rp.Invalidate(); }
break;
case Allapot.Gumi:
rp.Invalidate();
break;
case Allapot.Mozgat:
int dx = e.X - elozo.X, dy = e.Y - elozo.Y;
foreach (Vonas v in vonasok.Where(x => x.Kijelolt))
{
for (int i = 0; i < v.Pontok.Count; i++) { v.Pontok[i] = new Point(v.Pontok[i].X + dx, v.Pontok[i].Y + dy); }
}
elozo = e.Location;
rp.Invalidate();
break;
}
}
private void rp_MouseUp(object sender, MouseEventArgs e)
{
if (allapot == Allapot.Gumi)
{
Rectangle keret = PontokbolRect(kezdo, e.Location);
foreach (Vonas v in vonasok)
{
if (keret.IntersectsWith(v.Hatar())) { v.Kijelolt = true; }
}
}
Veg();
}
private void rp_MouseCaptureChanged(object sender, EventArgs e)
{
if (allapot == Allapot.Gumi || allapot == Allapot.Mozgat) { Megszakit(); }
}
private void Veg()
{
allapot = Allapot.Nincs;
epulo = null;
mozgatasElott = null;
rp.Cursor = Cursors.Cross;
Frissit();
}
private void Megszakit()
{
if (allapot == Allapot.Mozgat && mozgatasElott != null)
{
int i = 0;
foreach (Vonas v in vonasok.Where(x => x.Kijelolt)) { v.Pontok = new List<Point>(mozgatasElott[i++]); } // vissza a kezdő helyre
}
Veg();
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Escape && (allapot == Allapot.Gumi || allapot == Allapot.Mozgat)) { Megszakit(); return true; }
if (keyData == (Keys.Control | Keys.Z) && vonasok.Count > 0 && allapot == Allapot.Nincs) { vonasok.RemoveAt(vonasok.Count - 1); Frissit(); return true; }
if (keyData == Keys.Delete && allapot == Allapot.Nincs) { vonasok.RemoveAll(v => v.Kijelolt); Frissit(); return true; }
return base.ProcessCmdKey(ref msg, keyData);
}
private Rectangle PontokbolRect(Point p, Point q)
{
return new Rectangle(Math.Min(p.X, q.X), Math.Min(p.Y, q.Y), Math.Abs(p.X - q.X), Math.Abs(p.Y - q.Y));
}
private void Frissit()
{
tsslDb.Text = vonasok.Count + " vonal, " + vonasok.Count(v => v.Kijelolt) + " kijelölve";
rp.Invalidate();
}
private void Rajzol(Graphics g)
{
g.SmoothingMode = SmoothingMode.AntiAlias;
foreach (Vonas v in vonasok)
{
using (Pen toll = new Pen(v.Szin, v.Vastagsag))
using (Brush b = new SolidBrush(v.Szin))
{
toll.StartCap = toll.EndCap = LineCap.Round;
toll.LineJoin = LineJoin.Round;
if (v.Pontok.Count == 1) { g.FillEllipse(b, v.Pontok[0].X - v.Vastagsag / 2, v.Pontok[0].Y - v.Vastagsag / 2, v.Vastagsag, v.Vastagsag); }
else { g.DrawLines(toll, v.Pontok.ToArray()); }
}
}
}
private void rp_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.Clear(Color.White);
Rajzol(g);
foreach (Vonas v in vonasok.Where(x => x.Kijelolt))
{
using (Pen szagg = new Pen(Color.RoyalBlue) { DashStyle = DashStyle.Dash })
{
Rectangle h = v.Hatar();
g.DrawRectangle(szagg, h.X, h.Y, h.Width, h.Height);
}
}
if (allapot == Allapot.Gumi)
{
Rectangle keret = PontokbolRect(kezdo, aktualis);
using (Pen szagg = new Pen(Color.RoyalBlue) { DashStyle = DashStyle.Dash })
using (Brush halvany = new SolidBrush(Color.FromArgb(40, Color.RoyalBlue)))
{
g.FillRectangle(halvany, keret);
g.DrawRectangle(szagg, keret.X, keret.Y, keret.Width, keret.Height);
}
}
}
private void btnMentes_Click(object sender, EventArgs e)
{
sfd.Filter = "PNG (*.png)|*.png";
if (sfd.ShowDialog(this) != DialogResult.OK) { return; }
using (Bitmap kep = new Bitmap(rp.Width, rp.Height))
{
using (Graphics g = Graphics.FromImage(kep))
{
g.Clear(Color.White);
Rajzol(g);
}
kep.Save(sfd.FileName, ImageFormat.Png);
}
}Mit tegyél ide: a kész rajzoló: több színes vonal, egy kijelölt (szaggatott kerettel), a gumi-téglalap és az állapotsor.Javasolt fájlnév: winforms-9-6-kesz.webp · Alt szöveg: A kész egéres rajzoló
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. Egyenes vonal Shift-tel
A vonalnál a Shift nyomva tartásával az előnézet vízszintes vagy függőleges legyen. Hogyan döntöd el az irányt? A nagyobb elmozdulás irányát választod: ha Egy lehetséges megoldás
|dx| > |dy|, az Y a kezdő Y marad, különben az X.Point veg = aktualis;if ((Control.ModifierKeys & Keys.Shift) == Keys.Shift){ if (Math.Abs(veg.X - kezdo.X) > Math.Abs(veg.Y - kezdo.Y)) { veg.Y = kezdo.Y; } else { veg.X = kezdo.X; }}g.DrawLine(toll, kezdo, veg);
2. Kattintás és húzás megkülönböztetése
Egy alakzat mozog, ha húzzák, de csak kijelölődik, ha egyszerűen rákattintanak. Mikor kezded a mozgatást? Csak akkor, ha az egér a lenyomás helyétől a Egy lehetséges megoldás
SystemInformation.DragSize küszöbnél messzebb mozdult. Addig a gombnyomás kattintás.Size kuszob = SystemInformation.DragSize;if (!mozog && (Math.Abs(e.X - kezdo.X) >= kuszob.Width / 2 || Math.Abs(e.Y - kezdo.Y) >= kuszob.Height / 2)){ mozog = true; // innentől húzás}
3. Jobb gombbal görgetés (pan)
A jobb gomb húzása a nézetet (a 9.5 lecke A pixel-elmozdulást világ-egységre (osztás a kozepVilag értékét) mozgassa. Mit kell átszámolnod?Egy lehetséges megoldás
skala-val), és az Y előjele fordított: a nézet a mutatóval együtt mozog.if (e.Button == MouseButtons.Right && panelHuzas){ kozepVilag = new PointF(kozepVilag.X - (e.X - elozo.X) / skala, kozepVilag.Y + (e.Y - elozo.Y) / skala); elozo = e.Location; rp.Invalidate();}
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. Az események sorrendje
Melyik sorrendben érkeznek az események egy húzásnál?Megoldás megnyitása
MouseDown, több MouseMove, MouseUp (és ha a felhasználó egyszerűen kattint, előtte/utána a Click, MouseClick).
2. Melyik gomb?
Hogyan ellenőrzöd, hogy a jobb gomb váltotta ki az eseményt?Megoldás megnyitása
e.Button == MouseButtons.Right.
Közepes szint
3. Kör rajzolása középpontból
A kör középpontja a lenyomás helye, a sugara az egér távolsága. Hogyan számolod a befoglaló téglalapot? A sugár Megoldás megnyitása
(int)Math.Sqrt(dx² + dy²), a téglalap: (kezdo.X - r, kezdo.Y - r, 2r, 2r) (9.3 lecke).int dx = aktualis.X - kezdo.X, dy = aktualis.Y - kezdo.Y;int r = (int)Math.Sqrt(dx * dx + dy * dy);g.DrawEllipse(toll, kezdo.X - r, kezdo.Y - r, 2 * r, 2 * r);
4. Radír
A radír törölje a vonalakat, amelyeket az egér érint. Hogyan döntöd el, hogy egy vonal érintett-e? Egyszerű változat: az egér pontja a vonal befoglaló téglalapjában (Megoldás megnyitása
Hatar().Contains). Pontosabb: a pont és a szakaszok távolsága a vastagság felénél kisebb.
Kihívás szint
5. Pont–szakasz távolság
Írj függvényt, amely a pont és egy szakasz távolságát adja (pontos találat-vizsgálathoz). Mi a lényege? A pontot merőlegesen vetíted a szakaszra, a vetületet a szakasz végpontjai közé szorítod (Megoldás megnyitása
t 0 és 1 között), és a vetületi pont távolságát számolod.private double TavolsagSzakasztol(PointF p, PointF a, PointF b){ double dx = b.X - a.X, dy = b.Y - a.Y; double hossz2 = dx * dx + dy * dy; if (hossz2 == 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) / hossz2)); 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));}
6. Visszavonás mozgatás után
A Ctrl+Z a mozgatást is vonja vissza. Hogyan tárolod a visszavonási lépéseket? Egy Megoldás megnyitása
Stack<Lepes>, ahol a Lepes a művelet és a visszaállításhoz szükséges adat (törölt vonal, mozgatás előtti pontok). A visszavonás a verem tetejét veszi ki és fordítva végrehajtja (a 12. fejezet Command mintája).
Gyors önellenőrzés
Válaszolj fejben, és csak utána nyisd le a választ. A Lenyomott gombbal az események akkor is a vezérlőhöz jönnek, ha a mutató elhagyja; a koordináta lehet a határon kívüli. A húzás közbeni előnézet; csak a Így a megfogott pont az egér alatt marad, az alakzat nem ugrik. Legalább két pontot kér; egy pontnál külön kört rajzolunk. A Form 1. Mi a három esemény szerepe?
MouseDown: a húzás kezdete; MouseMove: folyamat; MouseUp: vége és véglegesítés.2. Miért kell a húzás jelző?
MouseMove gomb nélkül is fut: a jelző mondja meg, húzás része-e.3. Mit jelent az egér rögzítése?
4. Mi a gumivonal?
Paint rajzolja, a MouseUp teszi a modellbe.5. Miért a különbség módszerrel mozgatsz?
6. Miért nem rajzol a DrawLines egy pontot?
7. Hogyan szakítod meg a húzást Esc-pel?
ProcessCmdKey metódusában (vagy KeyPreview); a jelzőt hamisra állítod, és szükség esetén visszaállítod az eredeti állapotot.
Mit tudsz már?
- A három egéreseményt állapotgépként kapcsolod össze.
- Szabadkézi vonalat, gumivonalat és kijelölő téglalapot készítesz.
- Alakzatot mozgatsz a különbség módszerrel, és találatot vizsgálsz.
- Kezeled az egér rögzítését, a megszakítást és a gombokat.
- A rajz adatait tárolod, és a
Paintrajzolja újra.
Következő lecke: Alakzatok rajzolása és újrarajzolása
Az alakzatok objektumorientált modellje: közös ős, vonal-, téglalap-, ellipszis-osztály, rétegek, találat-vizsgálat és hatékony újrarajzolás.



