Grafikus C# – Windows Forms tanfolyam › 9.6 lecke

Windows Forms · 9.6 lecke · egéres rajzolás

MouseDown, MouseMove, MouseUp

Minden rajzolóprogram, játék és tervező ugyanazt a mintát használja: lenyomod az egérgombot (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.

  • kb. 120 perc
  • közepes–haladó szint
  • egéres húzás
Anime stílusú fiú és lány egy C# programozási feladatot beszél meg

A lecke végére

  • a MouseDown, MouseMove, MouseUp eseményeket állapotgépként (huzas jelző) 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.

9.5 Pontok, koordináták →

A három esemény és az állapotgép

EseményMikor?Mit teszünk benne?
MouseDownAz 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.
MouseMoveAz egér mozgásakor (gomb nélkül is!).Ha huzas igaz: a húzás folyamata (új pont, előnézet), és Invalidate().
MouseUpA gomb elengedésekor.A húzás vége: az eredmény véglegesítése, huzas = false.
Form1.csC#
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();
}
A jelző szerepeA 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.
Az egér rögzítéseA gomb lenyomásakor a vezérlő automatikusan rögzíti az egeret: a 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.
GombokAz 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).
Módosító billentyűkA 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):

Form1.cs (using System.Collections.Generic; using System.Drawing.Drawing2D;)C#
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());
}
}
}
}
PontlistaA vonal pontok sorozata; a 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.
Miért ritkítunk?Gyors húzásnál az egér pontjai távol esnek egymástól (a vonal szögletes), lassúnál nagyon sűrűk (felesleges, nehéz a lista). A 2 pixeles küszöb a lényegtelen pontokat kihagyja.
Egyetlen kattintásA DrawLines legalább két pontot kér (ArgumentException egynél). Az egy pontos vonalat pontként (kitöltött körként) rajzoljuk.
Raszteres változatFestőprogramban gyakori a 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:

Form1.csC#
// 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)
}
}
Előnézet és modellA húzás közbeni alakzat csak előnézet: nem kerül a listába. Ha a felhasználó az Esc billentyűvel megszakítja, egyszerűen nem csinálunk semmit. A véglegesítés a MouseUp-ban történik.
Fordított húzásHa a felhasználó jobbról balra vagy lentről felfelé húz, a szélesség/magasság negatív lenne. A PontokbolRect (Min, Abs) mindig érvényes téglalapot ad.
Befoglaló téglalapA kijelöléshez a vonal befoglaló téglalapját (a pontok legkisebb és legnagyobb X, Y értéke) használjuk, és az IntersectsWith dönt. Egyszerű és gyors; pontosabb vizsgálat a vonal szakaszainak metszése.
Gyors vonal-előnézetA vonalnál és az ellipsziszisnél ugyanaz a minta: a 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):

Form1.csC#
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;
}
Különbség módszerMinden 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.
Legfelső találatA később rajzolt alakzat takar. Találatkeresésnél a lista végétől visszafelé haladunk, és az elsőnél megállunk (a 9.7 lecke).
Point struktúraA listában tárolt Point csak egészben cserélhető (lista[i] = new Point(...)); a lista[i].X += dx nem fordul le.
MutatóA 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.

📷 Képernyőkép helye

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étel
Hiba é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.

Ok és javítás

Negatív szélesség/magasság. Használd a 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.

Ok és javítás

Ha a 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.

Ok és javítás

Az egér események frekvenciája véges, gyors mozgásnál a pontok távol vannak. A 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.

Ok és javítás

A 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:

  1. Felület: rp (RajzPanel, Dock = Fill, Cursor = Cross); eszköztár: rbToll és rbKijelol (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.
  2. Vonas osztály: Pontok, Szin, Vastagsag, Kijelolt, Hatar() (a pontok befoglaló téglalapja a vonal vastagságával bővítve).
  3. 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.
  4. 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.
  5. Mozgatás: a kijelölt vonalak a különbség módszerrel mozognak; mozgatás közben SizeAll mutató; a mozgatás a panel határain belülre korlátozott (a vonalak nem szöknek ki teljesen).
  6. 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.
  7. 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).
  8. Visszavonás: Ctrl+Z az utolsó vonalat törli; Delete a kijelölteket.
  9. Mentés: a vonalak kirajzolása egy Bitmapra (fehér háttér), mentés PNG-be (SaveFileDialog, 9.2 lecke).
  10. 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.

Form1.cs (using System.Collections.Generic; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Linq;)C#
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);
}
}
📷 Képernyőkép helye

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?

Egy lehetséges megoldás

A nagyobb elmozdulás irányát választod: ha |dx| > |dy|, az Y a kezdő Y marad, különben az X.

Form1.csC#
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?

Egy lehetséges megoldás

Csak akkor, ha az egér a lenyomás helyétől a SystemInformation.DragSize küszöbnél messzebb mozdult. Addig a gombnyomás kattintás.

Form1.csC#
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 kozepVilag értékét) mozgassa. Mit kell átszámolnod?

Egy lehetséges megoldás

A pixel-elmozdulást világ-egységre (osztás a skala-val), és az Y előjele fordított: a nézet a mutatóval együtt mozog.

Form1.csC#
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?

Megoldás megnyitása

A sugár (int)Math.Sqrt(dx² + dy²), a téglalap: (kezdo.X - r, kezdo.Y - r, 2r, 2r) (9.3 lecke).

Form1.csC#
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?

Megoldás megnyitása

Egyszerű változat: az egér pontja a vonal befoglaló téglalapjában (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?

Megoldás megnyitása

A pontot merőlegesen vetíted a szakaszra, a vetületet a szakasz végpontjai közé szorítod (t 0 és 1 között), és a vetületi pont távolságát számolod.

Form1.csC#
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?

Megoldás megnyitása

Egy 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.

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ő?

A 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?

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.

4. Mi a gumivonal?

A húzás közbeni előnézet; csak a Paint rajzolja, a MouseUp teszi a modellbe.

5. Miért a különbség módszerrel mozgatsz?

Így a megfogott pont az egér alatt marad, az alakzat nem ugrik.

6. Miért nem rajzol a DrawLines egy pontot?

Legalább két pontot kér; egy pontnál külön kört rajzolunk.

7. Hogyan szakítod meg a húzást Esc-pel?

A Form 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 Paint rajzolja ú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.