Grafikus C# – Windows Forms tanfolyam › 9.9 lecke

Windows Forms · 9.9 lecke · adatvizualizáció

Grafikonok és adatok szemléltetése

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

  • kb. 120 perc
  • haladó szint
  • diagramok
Diáklány C# programot készít laptopon

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 Chart vezé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.

9.8 Erőforrások felszabadítása →

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:

Form1.csC#
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);
}
Miért szép lépés?Ha a legnagyobb érték 37,4, a tengely 0–40 (lépés 10) olvashatóbb, mint 0–37,4 (lépés 7,48). A SzepLepes a nyers lépésközt (tartomány / 5) a legközelebbi 1, 2, 5 vagy 10 szorzatra kerekíti.
A nullaOszlopdiagramnál a tengely a nulláról induljon (a levágott tengely az arányokat torzítja). A Math.Min(0, ...) és Math.Max(0, ...) ezt biztosítja negatív értéknél is.
Szélsőséges esetHa minden érték nulla (vagy egyenlő), a tartomány nulla lenne, és a (max − min) osztás nullával NaN-t adna. A kód ilyenkor mesterségesen 1-re tágítja.
MargókA rajzterület a keret és a margók (bal: tengely-feliratok, alul: kategórianevek, felül: cím) levonása után marad. Átméretezéskor újra számolódik (a 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

Form1.cs (using System.Linq; using System.Drawing.Drawing2D;)C#
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);
}
}
}
Sáv és oszlopAz oszlopok az 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.
Az érték felirataAz oszlop magassága becsülhető, de a pontos szám kiírva hasznos (és hozzáférhetőbb, 6.9 lecke). Az érték a negatív oszlop alatt is látszik: a felso a nulla vonalhoz mért szél.
KategórianévA 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.
VonaldiagramIdősorokra (hőmérséklet, havi eladás) használd, ahol a pontok sorrendje számít; kategóriákra (tantárgyak) az oszlopdiagram jobb, mert a vonal összefüggést sugall ott, ahol nincs.

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:

Form1.csC#
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 és a szögekA 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ő).
SzázalékAz 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.
Csak pozitív értékA kördiagram a részek összegét jeleníti meg: negatív vagy nulla értéknek nincs szelete. Ez nem hiba, de a felhasználót tájékoztatni kell.
Kis szeletekAz 1–2%-os szeletek feliratai összeérnek. Összevonhatod az apró szeleteket „Egyéb” csoportba, vagy a feliratot csak egy küszöb felett írod ki, és jelmagyarázatot adsz.

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.

KontrasztA feliratok és a háttér kontrasztja legalább 4,5 : 1 legyen (6.9 lecke). Oszlopon belüli szöveg (fehér a világoskéken) gyakran gyenge: tedd az értéket az oszlop fölé fekete színnel.
JelmagyarázatHa a szeletek feliratai nem férnek ki, rajzolj jelmagyarázatot: színes négyzet + név + érték. A sorrend egyezzen a szeletekével.
Cím és tengelyfeliratA diagramnak legyen címe és mértékegysége (Havi csapadék (mm)). A cím mondja el, mit mutat, nem az, hogy diagram.
Torzítás kerüléseAz oszlopdiagram tengelye nulláról induljon; a 3D diagram és az ellipszis alakú kör torzítja az arányokat: kerüld.

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

Form1.cs (using System.Windows.Forms.DataVisualization.Charting;)C#
// 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ásChart vezérlő
ElőnyTeljes 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ánySok 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.

📷 Képernyőkép helye

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.

Ok és javítás

Az Y megfordítása hiányzik (9.5 lecke). A magasság a 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étel
Hiba é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.

Ok és javítás

A tengely felső határa a mért legnagyobb érték, nem a kerek felső határ. Használd a 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 összegben
Hiba é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.

Ok és javítás

A felirat a sávnál szélesebb. Használj 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.

Ok és javítás

A mentés a képernyőn látott méretet adja. Nagyobb felbontású mentéshez rajzold a diagramot egy nagyobb 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:

  1. 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 oldalon rp (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.
  2. Adat: az Adat osztá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.
  3. Rajzolás: a három rajzoló metódus (OszlopRajzol, VonalRajzol, KorRajzol) egyetlen Diagram(Graphics g, Rectangle terulet, List<Adat> adatok) metódusból hívva; a cím a tetején; a tengely a Tengely (szép lépés) alapján.
  4. 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.
  5. 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 gombok AccessibleName értékkel; a cím és a mértékegység.
  6. Egér: az oszlop fölé mutatva az lblErtek mutatja 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.
  7. 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).
  8. Erőforrások: minden toll, ecset, betű, StringFormat using blokkban vagy mezőben, a FormClosed-ben felszabadítva (9.8 lecke).
  9. 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.

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

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?

Egy lehetséges megoldás

A rajzterület jobb szélén: soronként 22 pixel; a négyzet 14 × 14, mellette a szöveg.

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

Egy lehetséges megoldás

Az átlag (adatok.Average) Y-koordinátája az YKoordinata metódussal; szaggatott toll (DashStyle.Dash), a felirat a vonal végén.

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

Egy lehetséges megoldás

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.

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?

Megoldás megnyitása

(a) vonal (időbeli változás); (b) kör (arány); (c) oszlop (kategóriák összehasonlítása).

Közepes szint

3. Szép lépés

Mekkora a SzepLepes(0.37, 5) értéke?

Megoldás megnyitása

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.

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?

Megoldás megnyitása

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

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?

Megoldás megnyitása

A hiányzó (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?

Megoldás megnyitása

A kattintás szögét 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).

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

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?

Az 1, 2, 5 × 10n lépésköz, amellyel a tengely kerek számokon oszlik.

3. Miért induljon a nulláról az oszlopdiagram tengelye?

A levágott tengely az arányokat torzítja (a kis különbség nagynak látszik).

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?

A színtévesztők és a fekete-fehér nyomtatás miatt: legyen felirat, érték, jelmagyarázat, minta.

6. Mit kell ellenőrizni a Paint elején?

Az üres adatot (Count == 0), és a kis méretet: különben Min/Max kivétel, nulla osztás.

7. Mikor jobb a Chart vezérlő?

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.

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 Chart vezé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.