Grafikus C# – Windows Forms tanfolyam › 9.9 lecke Windows Forms · 9.9 lecke · adatvizualizáció Egy táblázat számai sokszor nem mondanak semmit, egy jól megrajzolt diagram viszont egy pillantással elárulja a trendet, a legnagyobb értéket, az arányokat. A grafikon készítése a rajzolás összes eddigi eszközét összegzi: Grafikonok és adatok szemléltetése
Paint, Graphics, koordináta-átszámítás (9.5), tollak és ecsetek (9.4). Ebben a leckében saját oszlop-, vonal- és kördiagramot rajzolsz: kiszámolod a „szép” tengelybeosztást, elhelyezed a feliratokat és a jelmagyarázatot, kezeled a szélsőséges eseteket (üres adat, negatív érték), és hozzáférhető, színtévesztőknek is használható színeket választasz. Megnézzük a beépített Chart vezérlőt is.
A lecke végére
- az adatok értékét képernyő-koordinátára számolod (érték → pixel), margókkal és rajzterülettel;
- „szép” tengelybeosztást számolsz (1, 2, 5 × 10n lépés), és feliratozod a tengelyeket;
- oszlop-, vonal- és kördiagramot rajzolsz (
FillRectangle,DrawLines,FillPie); - kezeled a szélsőséges eseteket (üres adat, nulla, negatív érték, hosszú felirat);
- hozzáférhető színeket és feliratozást használsz (nem csak színnel különböztetsz);
- egérrel mutatod az oszlop értékét (találat-vizsgálat), és mentesz képet;
- ismered a
Chartvezérlő lehetőségeit és korlátait.
Mielőtt elkezded
Ehhez a leckéhez a 9.3–9.6 lecke (Paint, Pen, Brush, koordináták, egér), a 8.3 lecke (DataGridView) és a 6.9 lecke (hozzáférhetőség) szükséges.
Rajzterület, értékek és „szép” tengelybeosztás
A diagram két rétege: a keret (címek, tengely-feliratok, margók) és a rajzterület (plot), amelyen az adat jelenik meg. Az értékeket a rajzterületre számoljuk át (9.5 lecke elve): y = plot.Bottom − (érték − min) / (max − min) · plot.Height. A tengely határai kerek számok legyenek (0, 20, 40…), nem a mért legnagyobb érték (37,4): ezt a „szép” lépés adja:
public class Adat
{
public string Nev { get; set; } = "";
public double Ertek { get; set; }
}
// „szép” lépésköz: 1, 2 vagy 5 szorozva 10 hatványával
private double SzepLepes(double tartomany, int celDarab)
{
if (tartomany <= 0) { return 1; }
double nyers = tartomany / celDarab;
double nagysag = Math.Pow(10, Math.Floor(Math.Log10(nyers)));
double tort = nyers / nagysag;
double szep = tort <= 1 ? 1 : tort <= 2 ? 2 : tort <= 5 ? 5 : 10;
return szep * nagysag;
}
// a tengely határai: a 0-t mindig tartalmazza (az oszlopdiagram alapja)
private void Tengely(List<Adat> adatok, out double min, out double max, out double lepes)
{
double legkisebb = Math.Min(0, adatok.Min(a => a.Ertek));
double legnagyobb = Math.Max(0, adatok.Max(a => a.Ertek));
if (legnagyobb == legkisebb) { legnagyobb = legkisebb + 1; } // csupa nulla: legyen tartomány
lepes = SzepLepes(legnagyobb - legkisebb, 5);
min = Math.Floor(legkisebb / lepes) * lepes;
max = Math.Ceiling(legnagyobb / lepes) * lepes;
}
// érték → képernyő Y a rajzterületen
private float YKoordinata(double ertek, double min, double max, Rectangle plot)
{
return (float)(plot.Bottom - (ertek - min) / (max - min) * plot.Height);
}SzepLepes a nyers lépésközt (tartomány / 5) a legközelebbi 1, 2, 5 vagy 10 szorzatra kerekíti.Math.Min(0, ...) és Math.Max(0, ...) ezt biztosítja negatív értéknél is.(max − min) osztás nullával NaN-t adna. A kód ilyenkor mesterségesen 1-re tágítja.Paint-ben).Jósolj!
Az adatok legnagyobb értéke 73, a legkisebb 12. Mi a Tengely által adott min, max és lepes?
Jóslat ellenőrzése
A legkisebb a Math.Min(0, 12) = 0, a legnagyobb 73: a tartomány 73, a nyers lépés 14,6; a nagyságrend 10, a tört 1,46 → 2, tehát a lépés 20; min = 0, max = ceil(73 / 20) · 20 = 80.
Oszlop- és vonaldiagram
private Color[] paletta =
{
Color.FromArgb(0, 114, 178), Color.FromArgb(230, 159, 0), Color.FromArgb(0, 158, 115), Color.FromArgb(213, 94, 0),
Color.FromArgb(204, 121, 167), Color.FromArgb(86, 180, 233), Color.FromArgb(120, 120, 120)
};
private Rectangle RajzTerulet(Rectangle kereta)
{
return new Rectangle(kereta.X + 60, kereta.Y + 40, Math.Max(10, kereta.Width - 80), Math.Max(10, kereta.Height - 90));
}
private void OszlopRajzol(Graphics g, Rectangle plot, List<Adat> adatok)
{
double min, max, lepes;
Tengely(adatok, out min, out max, out lepes);
using (Pen racs = new Pen(Color.Gainsboro))
using (StringFormat jobbra = new StringFormat { Alignment = StringAlignment.Far, LineAlignment = StringAlignment.Center })
{
// vízszintes rácsvonalak és a tengely-feliratok
for (double v = min; v <= max + lepes / 2; v += lepes)
{
float y = YKoordinata(v, min, max, plot);
g.DrawLine(racs, plot.Left, y, plot.Right, y);
g.DrawString(v.ToString("0.##"), this.Font, Brushes.Black, new RectangleF(0, y - 10, plot.Left - 6, 20), jobbra);
}
}
float nulla = YKoordinata(0, min, max, plot);
g.DrawLine(Pens.Black, plot.Left, nulla, plot.Right, nulla); // a nulla vonal
float hely = (float)plot.Width / adatok.Count; // egy oszlop „sávja”
using (StringFormat kozep = new StringFormat { Alignment = StringAlignment.Center, Trimming = StringTrimming.EllipsisCharacter, FormatFlags = StringFormatFlags.NoWrap })
{
for (int i = 0; i < adatok.Count; i++)
{
float x = plot.Left + i * hely + hely * 0.15f;
float szeles = hely * 0.7f;
float y = YKoordinata(adatok[i].Ertek, min, max, plot);
float felso = Math.Min(y, nulla);
float magas = Math.Abs(nulla - y);
using (Brush b = new SolidBrush(paletta[i % paletta.Length]))
{
g.FillRectangle(b, x, felso, szeles, magas);
}
// érték az oszlop fölött (ne csak a magasság sejtesse: szám is legyen)
g.DrawString(adatok[i].Ertek.ToString("0.##"), this.Font, Brushes.Black, new RectangleF(x - 10, felso - 18, szeles + 20, 16), kozep);
// kategórianév alul, hosszú név „…”-tal rövidítve
g.DrawString(adatok[i].Nev, this.Font, Brushes.Black, new RectangleF(plot.Left + i * hely, plot.Bottom + 6, hely, 18), kozep);
}
}
}
// vonaldiagram: ugyanaz a skála, az értékek pontjait összekötjük
private void VonalRajzol(Graphics g, Rectangle plot, List<Adat> adatok, double min, double max)
{
float hely = (float)plot.Width / adatok.Count;
PointF[] pontok = new PointF[adatok.Count];
for (int i = 0; i < adatok.Count; i++)
{
pontok[i] = new PointF(plot.Left + hely * (i + 0.5f), YKoordinata(adatok[i].Ertek, min, max, plot));
}
using (Pen toll = new Pen(paletta[0], 3))
{
if (pontok.Length > 1) { g.DrawLines(toll, pontok); }
foreach (PointF p in pontok)
{
g.FillEllipse(Brushes.White, p.X - 5, p.Y - 5, 10, 10); // jelölő körök
g.DrawEllipse(toll, p.X - 5, p.Y - 5, 10, 10);
}
}
}adatok.Count egyenlő sávra osztják a rajzterület szélességét; az oszlop a sáv 70%-a, a maradék a hézag. Így az oszlopok száma változhat, a kép mindig kitölti a területet.felso a nulla vonalhoz mért szél.StringFormat Trimming = EllipsisCharacter beállítása a hosszú nevet „…”-tal rövidíti, a NoWrap nem törik sorba. Sok oszlopnál a nevek összeérnek: elforgatott felirat (RotateTransform) vagy kevesebb felirat kell.Jósolj!
A diagram üres adatlistával (adatok.Count == 0) hívja az OszlopRajzol metódust. Mi történik?
Jóslat ellenőrzése
A adatok.Min(...) üres listán InvalidOperationException kivételt dob (a sorozat üres), és a Paint kezelőben ez „piros X”-et okoz a vezérlőn. A Paint elején ellenőrizd a Count értékét, és üres adatnál írj „Nincs megjeleníthető adat.” üzenetet.
Kördiagram
A kördiagram arányokat mutat (a rész az egészből). Minden szelet szöge az érték és az összeg hányadosa: sweep = 360 · érték / összeg. A FillPie a kezdő szögtől (startAngle, fokban, 0° a jobb oldal, az óramutató járásával megegyező irányban nő) rajzolja a szeletet; a felső (12 óra) kezdéshez −90°-ról indulunk:
private void KorRajzol(Graphics g, Rectangle terulet, List<Adat> adatok)
{
// csak a pozitív értékeknek van szelete
List<Adat> pozitiv = adatok.Where(a => a.Ertek > 0).ToList();
double osszeg = pozitiv.Sum(a => a.Ertek);
if (pozitiv.Count == 0 || osszeg <= 0)
{
g.DrawString("Nincs pozitív érték a kördiagramhoz.", this.Font, Brushes.Black, terulet.X + 10, terulet.Y + 10);
return;
}
int atmero = Math.Max(20, Math.Min(terulet.Width, terulet.Height) - 120); // hely a feliratoknak
Rectangle kor = new Rectangle(terulet.X + (terulet.Width - atmero) / 2, terulet.Y + (terulet.Height - atmero) / 2, atmero, atmero);
PointF kozep = new PointF(kor.X + atmero / 2f, kor.Y + atmero / 2f);
float kezdet = -90; // 12 óránál kezdünk
for (int i = 0; i < pozitiv.Count; i++)
{
float iv = (float)(360 * pozitiv[i].Ertek / osszeg);
using (Brush b = new SolidBrush(paletta[i % paletta.Length]))
using (Pen keret = new Pen(Color.White, 2))
{
g.FillPie(b, kor, kezdet, iv);
g.DrawPie(keret, kor, kezdet, iv); // fehér elválasztó a szeletek között
}
// felirat a szelet közepénél, a körön kívül: név és százalék (nem csak szín!)
double kozepSzog = (kezdet + iv / 2) * Math.PI / 180;
float lx = kozep.X + (float)((atmero / 2 + 14) * Math.Cos(kozepSzog));
float ly = kozep.Y + (float)((atmero / 2 + 14) * Math.Sin(kozepSzog));
string szoveg = pozitiv[i].Nev + " (" + (pozitiv[i].Ertek / osszeg).ToString("P0") + ")";
using (StringFormat sf = new StringFormat { Alignment = Math.Cos(kozepSzog) >= 0 ? StringAlignment.Near : StringAlignment.Far, LineAlignment = StringAlignment.Center })
{
g.DrawString(szoveg, this.Font, Brushes.Black, lx, ly, sf);
}
kezdet += iv;
}
}FillPie(ecset, téglalap, kezdőSzög, ívSzög) a befoglaló téglalapú körből metszi ki a szeletet. A szögek fokban vannak, 0° a jobb oldal, és az óramutató irányában nőnek (az Y lefelé nő).ToString("P0") a hányadost százalékként írja (0,25 → 25 %, a gép kultúrájának jelölésével). A felirat a név mellett a számot is mondja.Jósolj!
Négy érték: 10, 20, 30, 40. Mekkora az első szelet szöge, és hol végződik a második szelet (a −90° kezdettől számítva)?
Jóslat ellenőrzése
Az összeg 100: az első szelet 36° (360 · 10 / 100), a második 72°. A második szelet a −90° + 36° + 72° = 18°-nál végződik (3 óra felett kicsivel). A szeletek összege mindig 360°.
Színek és hozzáférhetőség a diagramon
Színtévesztés
A férfiak kb. 8%-a valamilyen színtévesztő; a piros–zöld párosítást nem különbözteti meg. Használj színtévesztőknek is megfelelő palettát (a fenti Okabe–Ito paletta: kék, narancs, zöld, vörös-narancs, rózsaszín, világoskék), és ne egyedül a szín hordozza az információt.
Mást is használj a színen kívül
Feliratok (név, érték, százalék), jelmagyarázat, sraffozás (HatchBrush), vonalstílus (szaggatott/folyamatos) segíti az értelmezést. Fekete-fehér nyomtatásnál is olvasható marad.
Jósolj!
A kördiagramban a szeletek piros és zöld színűek, felirat nélkül, jelmagyarázat nélkül. Mit lát egy pirosat és zöldet nem megkülönböztető felhasználó?
Jóslat ellenőrzése
Két közel azonos, barnás-szürkés szeletet, amelyeknek a jelentését nem tudja azonosítani. Feliratokkal (név és százalék) és színtévesztőbarát palettával a diagram mindenkinek használható marad.
A beépített Chart vezérlő
A .NET Framework Windows Forms projektekben a Toolbox-ban megtalálható a Chart vezérlő (System.Windows.Forms.DataVisualization.Charting), amely kész diagramtípusokat (oszlop, vonal, kör, pont, terület…), tengelyeket, jelmagyarázatot és egérinterakciót kínál. Újabb .NET (5 és fölötti) projektben a vezérlő nem része az alap keretrendszernek; külön NuGet-csomagként érhető el (a támogatása és a verzió-kompatibilitás változhat, ezért a használat előtt ellenőrizd a hivatalos csomagot és a dokumentációt):
// a diagram elkészítése: egy Chart vezérlő (chart1) a Formon
chart1.Series.Clear();
Series sorozat = chart1.Series.Add("Eladás");
sorozat.ChartType = SeriesChartType.Column; // Column (oszlop), Line, Pie, Bar, Area...
sorozat.Points.AddXY("Január", 120);
sorozat.Points.AddXY("Február", 95);
sorozat.Points.AddXY("Március", 140);
chart1.Titles.Clear();
chart1.Titles.Add("Havi eladás (db)");
chart1.ChartAreas[0].AxisY.Minimum = 0;
sorozat.IsValueShownAsLabel = true; // az értékek feliratként| Saját rajzolás | Chart vezérlő | |
|---|---|---|
| Előny | Teljes irányítás, kevés függőség, kis méret, megérted a működést. | Kész diagramtípusok, tengelyek, jelmagyarázat, nagyítás, egérkezelés; gyorsan elkészül. |
| Hátrány | Sok kódot kell írni (tengely, felirat, szélsőséges esetek). | Verziófüggés (újabb .NET-ben külön csomag), nagy vezérlő, a kinézet testreszabása bonyolult lehet. |
| Mikor? | Egyszerű, egyedi diagram; tanulás; ha nem akarsz függőséget. | Sokféle, összetett diagram; gyors fejlesztés; ha a projekt támogatja. |
Mit érdemes választanod?
Tanulási céllal és egyszerű diagramokra a saját rajzolás a jó választás: érted, mi történik. Összetett, sokféle diagramot használó alkalmazásban a Chart vezérlő (vagy egy harmadik fél könyvtára) időt takarít meg. A két módszer ismerete együtt a legerősebb.
Mit tegyél ide: az adatszemléltető három diagramtípussal (oszlop, vonal, kör) ugyanazokra az adatokra, egymás mellett.Javasolt fájlnév: winforms-9-9-diagramok.webp · Alt szöveg: Oszlop-, vonal- és kördiagram ugyanazokkal az adatokkal
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 diagram fejjel lefelé van
A nagyobb értékű oszlop alacsonyabbnak látszik. Az Y megfordítása hiányzik (9.5 lecke). A magasság a Ok és javítás
plot.Bottom − arány · plot.Height képlettel számolódik: lefelé nő a képernyő-Y, tehát a nagyobb érték kisebb Y-t kap.
2. InvalidOperationException üres adatnál
double max = adatok.Max(a => a.Ertek); // üres listán kivételHiba és javítás
A Max és Min üres sorozatnál kivételt dob. Ellenőrizd a Count értékét a Paint elején, és írj „Nincs adat” üzenetet.
3. Az oszlopok kilógnak a keretből
A legmagasabb oszlop a diagram tetejére ér, vagy átlóg. A tengely felső határa a mért legnagyobb érték, nem a kerek felső határ. Használd a Ok és javítás
Tengely metódust (kerek max a legnagyobb érték fölött).
4. A kördiagram szeleteinek összege nem 360°
float iv = (float)(360 * ertek / adatok.Sum(a => a.Ertek)); // a negatív értékek is az összegbenHiba és javítás
A negatív értékek rontják az összeget. Csak a pozitív értékekkel számolj (Where(a => a.Ertek > 0)), és jelezd a kihagyottakat.
5. A feliratok egymásba érnek
Sok oszlopnál a kategórianevek olvashatatlanok. A felirat a sávnál szélesebb. Használj Ok és javítás
StringTrimming.EllipsisCharacter értéket, rövidebb nevet, vagy elforgatott feliratot (RotateTransform(-45)); nagyon sok adatnál csak minden n-edik feliratot írd ki.
6. A diagram szemcsés, ha nyomtatjuk vagy mentjük
A mentett kép pixeles. A mentés a képernyőn látott méretet adja. Nagyobb felbontású mentéshez rajzold a diagramot egy nagyobb Ok és javítás
Bitmapra (a rajzoló metódus Graphics és terület paramétert kap, ezért ez egyszerű), és a méretezéshez a 9.2 lecke módszerét használd.
Mini projekt: adatszemléltető
Készíts programot, amely egy szerkeszthető táblázat adatait három diagramtípusban mutatja:
- Felület: bal oldalon
dgv(DataGridView, két oszlop: Név, Érték;BindingList<Adat>-hoz kötve, sor hozzáadás/törlés), jobb oldalonrp(RajzPanel, 9.3 lecke); fent:cmbTipus(Oszlop, Vonal, Kör),txtCim(diagram címe),cmbMinta(Havi hőmérséklet, Tantárgyak kedveltsége, Napi lépésszám, Negatív értékekkel), Mentés PNG-be. - Adat: az
Adatosztály, a rács módosítása (CellValueChanged, sor törlése/hozzáadása:ListChanged) újrarajzoltatja a diagramot; az érték ellenőrzése (szám, 8.8 lecke): hibás értéknél a cella jelzi (ErrorText), a diagram az érvényes értékekkel dolgozik. - Rajzolás: a három rajzoló metódus (
OszlopRajzol,VonalRajzol,KorRajzol) egyetlenDiagram(Graphics g, Rectangle terulet, List<Adat> adatok)metódusból hívva; a cím a tetején; a tengely aTengely(szép lépés) alapján. - Szélsőséges esetek: üres adat (üzenet), egy elem, negatív értékek (oszlop a nulla vonalról lefelé, kör: kihagyja és jelzi), nagyon nagy és nagyon kicsi értékek, hosszú nevek, kis ablakméret.
- Hozzáférhetőség: színtévesztőbarát paletta; az értékek számmal is kiírva; a kördiagram szeletei névvel és százalékkal; a
cmbTipusés a gombokAccessibleNameértékkel; a cím és a mértékegység. - Egér: az oszlop fölé mutatva az
lblErtekmutatja a nevet és az értéket (találat-vizsgálat a sáv szélessége alapján, 9.6 lecke); a kiemelt oszlop keretet kap. - Mentés: a diagram egy 1200 × 800-as
Bitmapra rajzolva (a rajzoló metódusok újrahasználva, fehér háttérrel), PNG-be mentve (9.2 lecke). - Erőforrások: minden toll, ecset, betű,
StringFormatusingblokkban vagy mezőben, aFormClosed-ben felszabadítva (9.8 lecke). - Teszteld: minden adatminta és diagramtípus, szerkesztés közben a diagram frissülése, a szélsőséges esetek, ablak átméretezése, a mentett kép, a hover az oszlopokon.
Megoldás megnyitása (próbáld ki előbb önállóan!)
A rajzoló metódusok a lecke szerintiek; itt a vezérlés és a mentés áll. A diagram egy közös Diagram metódus, amelyet a panel és a mentő kép is használ (ugyanaz a kód két célra). Az OszlopTengelyek metódust az OszlopRajzol első feléből (a rácsvonalak és a tengely-feliratok ciklusa) emeld ki külön metódusba, hogy a vonaldiagram is használhassa.
private BindingList<Adat> adatok = new BindingList<Adat>();
private int kiemelt = -1;
private void Form1_Load(object sender, EventArgs e)
{
cmbTipus.Items.AddRange(new object[] { "Oszlop", "Vonal", "Kör" });
cmbTipus.SelectedIndex = 0;
cmbMinta.Items.AddRange(new object[] { "Havi hőmérséklet", "Tantárgyak kedveltsége", "Napi lépésszám", "Negatív értékekkel" });
cmbMinta.SelectedIndex = 0;
dgv.AutoGenerateColumns = false;
dgv.DataSource = adatok;
adatok.ListChanged += (s, ev) => rp.Invalidate();
}
private void cmbMinta_SelectedIndexChanged(object sender, EventArgs e)
{
adatok.RaiseListChangedEvents = false;
adatok.Clear();
switch (cmbMinta.SelectedIndex)
{
case 0:
string[] honapok = { "Jan", "Feb", "Már", "Ápr", "Máj", "Jún" };
double[] homerseklet = { 1.5, 3.2, 8.4, 13.1, 18.0, 21.3 };
for (int i = 0; i < honapok.Length; i++) { adatok.Add(new Adat { Nev = honapok[i], Ertek = homerseklet[i] }); }
break;
case 1:
adatok.Add(new Adat { Nev = "Matematika", Ertek = 7 });
adatok.Add(new Adat { Nev = "Informatika", Ertek = 14 });
adatok.Add(new Adat { Nev = "Testnevelés", Ertek = 11 });
adatok.Add(new Adat { Nev = "Történelem", Ertek = 5 });
break;
case 2:
string[] napok = { "H", "K", "Sz", "Cs", "P", "Szo", "V" };
int[] lepes = { 6200, 8400, 7100, 9800, 5300, 12100, 4000 };
for (int i = 0; i < napok.Length; i++) { adatok.Add(new Adat { Nev = napok[i], Ertek = lepes[i] }); }
break;
default:
adatok.Add(new Adat { Nev = "Nyereség", Ertek = 40 });
adatok.Add(new Adat { Nev = "Veszteség", Ertek = -25 });
adatok.Add(new Adat { Nev = "Egyéb", Ertek = 10 });
break;
}
adatok.RaiseListChangedEvents = true;
adatok.ResetBindings();
}
private void Diagram(Graphics g, Rectangle terulet, List<Adat> lista)
{
g.SmoothingMode = SmoothingMode.AntiAlias;
g.Clear(Color.White);
using (Font cim = new Font(this.Font.FontFamily, 13, FontStyle.Bold))
{
g.DrawString(txtCim.Text, cim, Brushes.Black, terulet.X + 10, terulet.Y + 8);
}
if (lista.Count == 0)
{
g.DrawString("Nincs megjeleníthető adat.", this.Font, Brushes.Black, terulet.X + 10, terulet.Y + 50);
return;
}
if (cmbTipus.SelectedIndex == 2)
{
KorRajzol(g, terulet, lista);
return;
}
Rectangle plot = RajzTerulet(terulet);
if (cmbTipus.SelectedIndex == 0)
{
OszlopRajzol(g, plot, lista);
if (kiemelt >= 0 && kiemelt < lista.Count)
{
float hely = (float)plot.Width / lista.Count;
using (Pen keret = new Pen(Color.Black, 2)) { g.DrawRectangle(keret, plot.Left + kiemelt * hely + hely * 0.1f, plot.Top, hely * 0.8f, plot.Height); }
}
}
else
{
double min, max, lepes;
Tengely(lista, out min, out max, out lepes);
OszlopTengelyek(g, plot, min, max, lepes); // a rács és a feliratok a lecke OszlopRajzol első felével azonos
VonalRajzol(g, plot, lista, min, max);
}
}
private List<Adat> ErvenyesAdatok()
{
return adatok.Where(a => !double.IsNaN(a.Ertek) && !double.IsInfinity(a.Ertek)).ToList();
}
private void rp_Paint(object sender, PaintEventArgs e)
{
Diagram(e.Graphics, rp.ClientRectangle, ErvenyesAdatok());
}
private void rp_MouseMove(object sender, MouseEventArgs e)
{
List<Adat> lista = ErvenyesAdatok();
Rectangle plot = RajzTerulet(rp.ClientRectangle);
int uj = -1;
if (cmbTipus.SelectedIndex != 2 && lista.Count > 0 && plot.Contains(e.Location))
{
uj = (int)((e.X - plot.Left) / ((double)plot.Width / lista.Count));
}
if (uj != kiemelt)
{
kiemelt = uj;
lblErtek.Text = uj >= 0 && uj < lista.Count ? lista[uj].Nev + ": " + lista[uj].Ertek.ToString("0.##") : "";
rp.Invalidate();
}
}
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(1200, 800))
using (Graphics g = Graphics.FromImage(kep))
{
int regiKiemelt = kiemelt;
kiemelt = -1; // mentett képen nincs kiemelés
Diagram(g, new Rectangle(0, 0, 1200, 800), ErvenyesAdatok());
kiemelt = regiKiemelt;
kep.Save(sfd.FileName, ImageFormat.Png);
}
}Mit tegyél ide: a kész adatszemléltető: a szerkeszthető táblázat és mellette az oszlopdiagram, a kiemelt oszloppal és az érték feliratával.Javasolt fájlnév: winforms-9-9-kesz.webp · Alt szöveg: A kész adatszemléltető program
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. Jelmagyarázat
Rajzolj jelmagyarázatot a kördiagram mellé: színes négyzet, név, érték. Hol számolod a helyét? A rajzterület jobb szélén: soronként 22 pixel; a négyzet 14 × 14, mellette a szöveg.Egy lehetséges megoldás
int x = terulet.Right - 170, y = terulet.Top + 40;for (int i = 0; i < lista.Count; i++){ using (Brush b = new SolidBrush(paletta[i % paletta.Length])) { g.FillRectangle(b, x, y + i * 22, 14, 14); } g.DrawString(lista[i].Nev + ": " + lista[i].Ertek.ToString("0.##"), this.Font, Brushes.Black, x + 20, y + i * 22 - 2);}
2. Átlagvonal
Az oszlopdiagramon egy szaggatott vonal mutatja az értékek átlagát, felirattal. Hogyan számolod a helyét? Az átlag (Egy lehetséges megoldás
adatok.Average) Y-koordinátája az YKoordinata metódussal; szaggatott toll (DashStyle.Dash), a felirat a vonal végén.double atlag = lista.Average(a => a.Ertek);float y = YKoordinata(atlag, min, max, plot);using (Pen szagg = new Pen(Color.DimGray) { DashStyle = DashStyle.Dash }){ g.DrawLine(szagg, plot.Left, y, plot.Right, y);}g.DrawString("Átlag: " + atlag.ToString("0.##"), this.Font, Brushes.DimGray, plot.Right - 90, y - 18);
3. Halmozott oszlop
Két sorozatot (például lányok és fiúk száma) egymás mellett rajzolnál minden kategóriánál. Mit módosítasz az oszlopsávon? Az adatmodell kapjon két értéket; a sáv oszlopszélessége a sorozatok számával osztódik, és a sorozatoknak külön színt (és jelmagyarázatot) adsz.Egy lehetséges megoldás
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. Érték → pixel
A rajzterület magassága 200 pixel, alja az Y = 300, a tengely 0–50. Hol van az Y koordináta a 20-as értéknél?Megoldás megnyitása
300 − 20 / 50 · 200 = 300 − 80 = 220.
2. Melyik diagram?
Melyik típus a megfelelő: (a) havi hőmérséklet; (b) osztály nemek szerinti aránya; (c) öt iskola létszáma? (a) vonal (időbeli változás); (b) kör (arány); (c) oszlop (kategóriák összehasonlítása).Megoldás megnyitása
Közepes szint
3. Szép lépés
Mekkora a A nyers lépés 0,074; a nagyságrend 0,01; a tört 7,4, ez 10-nél kisebb és 5-nél nagyobb → 10; a lépés 10 · 0,01 = 0,1.SzepLepes(0.37, 5) értéke?Megoldás megnyitása
4. Százalékok összege
A kördiagram százalékai kerekítve 99%-ot vagy 101%-ot adhatnak. Mi az oka, és mit tehetsz? A kerekítés miatti eltérés természetes. Egy tizedesjegyig kiírva kisebb, vagy megjegyzés (a kerekítés miatt az összeg eltérhet); a legnagyobb maradékos módszerrel pontos 100% állítható elő.Megoldás megnyitása
Kihívás szint
5. Idősor hézagokkal
A hőmérséklet-sorozatban hiányzó napok vannak (nincs mérés). Hogyan rajzolod a vonaldiagramot? A hiányzó (Megoldás megnyitása
double? null) értéknél megszakítod a vonalat: szakaszokra bontod a pontokat (mint a 9.5 függvényábránál), és a szakaszokat külön rajzolod. A hiányt ne nullával töltsd ki (az hamis adat).
6. Kattintás a kör szeletére
A kördiagram szeletére kattintva a program kiemeli (kicsit kihúzza). Hogyan döntöd el, melyik szeletre kattintottak? A kattintás szögét Megoldás megnyitása
Math.Atan2 adja a kör középpontjához képest, a -90° kezdet szerint normalizálva (0–360); a szeletek kumulált szögei között keresed. Előbb a távolságot ellenőrzöd (a sugáron belül van-e).double dx = p.X - kozep.X, dy = p.Y - kozep.Y;if (Math.Sqrt(dx * dx + dy * dy) > sugar) { return -1; }double szog = Math.Atan2(dy, dx) * 180 / Math.PI + 90; // a 12 órához (-90°) viszonyítvaif (szog < 0) { szog += 360; }double kum = 0;for (int i = 0; i < ertekek.Count; i++){ kum += 360 * ertekek[i] / osszeg; if (szog < kum) { return i; }}return -1;
Gyors önellenőrzés
Válaszolj fejben, és csak utána nyisd le a választ. Az 1, 2, 5 × 10n lépésköz, amellyel a tengely kerek számokon oszlik. A levágott tengely az arányokat torzítja (a kis különbség nagynak látszik). A színtévesztők és a fekete-fehér nyomtatás miatt: legyen felirat, érték, jelmagyarázat, minta. Az üres adatot ( Sokféle, összetett diagramnál és gyors fejlesztésnél, ha a projekt (és a .NET verzió) támogatja; egyszerű, egyedi diagramnál a saját rajzolás.1. Hogyan számolsz értékből képernyő-Y-t?
plot.Bottom − (érték − min) / (max − min) · plot.Height: az Y lefelé nő, ezért a kivonás.2. Mi a „szép” lépés?
3. Miért induljon a nulláról az oszlopdiagram tengelye?
4. Hogyan számolod a kördiagram szelet szögét?
360 · érték / összeg; a szeletek összege 360°.5. Miért ne csak színnel különböztess?
6. Mit kell ellenőrizni a Paint elején?
Count == 0), és a kis méretet: különben Min/Max kivétel, nulla osztás.7. Mikor jobb a Chart vezérlő?
Mit tudsz már?
- Érték → pixel átszámítással, „szép” tengelybeosztással rajzolsz diagramot.
- Oszlop-, vonal- és kördiagramot készítesz, feliratokkal és jelmagyarázattal.
- Kezeled a szélsőséges eseteket (üres adat, negatív érték, hosszú nevek).
- Hozzáférhető színeket és feliratozást használsz.
- Ismered a
Chartvezérlő előnyeit és korlátait.
Következő lecke: mini projekt – rajzolóprogram
A 9. fejezet záró projektje: egy teljes rajzolóprogram eszközökkel, rétegekkel, visszavonással, képbetöltéssel és mentéssel.




