Grafikus C# – Windows Forms tanfolyam › 4.5 lecke

Windows Forms · 4.5 · önálló gyakorlás

Önálló feladat V.: Egyszerű rajzolóalkalmazás

Eddig a vezérlőelemeket helyeztük el az ablakon. Most te rajzolsz: egy kis programot készítesz, amelyben egérrel lehet vonalat, téglalapot és ellipszist húzni, színt és vastagságot választani, radírozni, visszavonni és a képet PNG fájlba menteni. A rajzoláshoz néhány új fogalom kell (Bitmap, Graphics, Pen, egéresemények), amelyeket az első szakasz röviden összefoglal, hogy a feladat önállóan is megoldható legyen. A 9. fejezet később mélyebben tárgyalja ezeket.

  • 150 perc
  • önálló munka
  • 100 pontos feladat
Diák C# programozást gyakorol egy tanulósarokban

A feladat végére

  • rajzfelületet készítesz Bitmap és PictureBox segítségével;
  • az egéreseményekből (MouseDown, MouseMove, MouseUp) rajzolást építesz;
  • a Graphics objektummal vonalat, téglalapot és ellipszist rajzolsz Pen-nel;
  • az alakzatokhoz előnézetet adsz húzás közben (Paint esemény, Invalidate);
  • a rajzot PNG-be mented, és visszavonást készítesz;
  • a rajzolási erőforrásokat (Pen, Graphics, Bitmap) helyesen szabadítod fel.

Mielőtt elkezded

Ehhez a feladathoz az 1. fejezet vezérlőelemei (RadioButton, TrackBar, PictureBox), a 2.3 lecke (SaveFileDialog) és a 2.5 lecke (ColorDialog) kellenek. A rajzolás új, ezért az első szakasz elegendő háttérismeretet ad. Készíts új, tiszta projektet.

4.4 Számológép →

Amit tudnod kell a rajzoláshoz

A Windows Forms rajzolása a GDI+ technológiára épül. A legfontosabb fogalmak:

FogalomMi ez?Példa
BitmapEgy kép a memóriában, képpontok (pixelek) rácsa. Ez lesz a „vászon”, amelyre rajzolunk.new Bitmap(600, 400)
GraphicsA „rajzoló kéz”: egy felület (például egy Bitmap), amelyre alakzatokat rajzolhatsz.Graphics.FromImage(rajz)
PenA „toll”: a vonal színe és vastagsága.new Pen(Color.Blue, 3)
SolidBrushA „festékes ecset” a kitöltéshez.new SolidBrush(Color.Red)
Point, RectangleEgy pont (X, Y) és egy téglalap (X, Y, szélesség, magasság). Az origó a bal felső sarok, az Y lefelé nő.new Rectangle(10, 20, 100, 50)
EgéreseményekA MouseDown (lenyomás), MouseMove (mozgás) és MouseUp (felengedés) esemény megkapja az egér helyét (e.Location) és gombját (e.Button).if (e.Button == MouseButtons.Left)
Invalidate() és PaintAz Invalidate() jelzi, hogy a vezérlőt újra kell rajzolni; ekkor fut le a Paint esemény.pbRajz.Invalidate();
Form1.cs (gombnyomásra egy kék vonal és egy zöld téglalap)C#
private Bitmap rajz;

private void Form1_Load(object sender, EventArgs e)
{
rajz = new Bitmap(pbRajz.Width, pbRajz.Height);

using (Graphics g = Graphics.FromImage(rajz))
{
g.Clear(Color.White);
}

pbRajz.Image = rajz;
}

private void btnProba_Click(object sender, EventArgs e)
{
using (Graphics g = Graphics.FromImage(rajz))
using (Pen kek = new Pen(Color.Blue, 3))
using (Pen zold = new Pen(Color.Green, 2))
{
g.DrawLine(kek, 20, 20, 200, 120);
g.DrawRectangle(zold, 50, 150, 120, 60);
}

pbRajz.Invalidate(); // a PictureBox újrarajzolja magát a megváltozott képpel
}
A rajz a Bitmapon élA Graphics.FromImage a bitmapre rajzol, ezért a rajz megmarad, akkor is, ha az ablakot lekicsinyítik vagy eltakarják. A PictureBox csak megjeleníti a bitmapot.
using és DisposeA Graphics, Pen és Brush operációs rendszer erőforrást foglal. A using blokk a végén felszabadítja őket. Ha elmarad, a program memóriát és rajzolási erőforrást veszít.
PictureBox SizeMode: NormalA PictureBox alapértelmezett Normal módban a bitmap képpontjai 1:1 arányban jelennek meg, így az egér helye (e.Location) megegyezik a bitmap koordinátájával. Zoom vagy StretchImage módban a koordináták elcsúsznának.
Invalidate()A bitmap megváltozása után a PictureBox nem tudja magától, hogy újra kell rajzolnia magát. Az Invalidate() szól neki.

Jósolj!

A gombnyomás után a kék vonal a bitmapra került. Lekicsinyíted az ablakot, majd visszanagyítod. Eltűnik-e a vonal? Mi lenne, ha a vonalat a pbRajz.CreateGraphics() metódussal, közvetlenül a PictureBox felületére rajzolnád?

Jóslat ellenőrzése

A bitmapra rajzolt vonal megmarad, mert maga a kép tartalmazza. A CreateGraphics() a vezérlő felületére rajzol, amelyet az operációs rendszer bármikor újrafest (lekicsinyítés, eltakarás után): a vonal ilyenkor eltűnik. Ezért a tartós rajzot mindig a bitmapra tesszük, és a vezérlőre csak az átmeneti dolgokat (például az előnézetet) a Paint eseményben.

A feladat leírása és a pontozás

Készíts Rajzoló nevű Windows Forms alkalmazást. Az ablak közepén egy fehér rajzfelület (PictureBox) áll, fölötte vagy mellette az eszköztár: eszközválasztó (toll, vonal, téglalap, ellipszis, radír), színválasztó gomb a kiválasztott szín előnézetével, vastagságcsúszka, kitöltés jelölőnégyzet, Törlés, Visszavonás és Mentés gomb.

RészKövetelményPont
Alapok (40)Fehér rajzfelület Bitmap-pal és PictureBox-szal; a rajz megmarad az ablak lekicsinyítése után is.10
Szabadkézi rajz (toll) egérrel, folytonos vonallal (a gyors mozgásnál is összefüggő).15
Színválasztás ColorDialog-gal (a kiválasztott szín látszik egy panelen) és vastagság TrackBar-ral (1–30).15
Eszközök (30)Vonal, téglalap és ellipszis húzással, előnézettel húzás közben; az alakzat az egér felengedésekor kerül véglegesen a rajzra.15
Radír (fehér vastag toll) és Törlés gomb (a teljes rajz fehérre).8
Kitöltés jelölőnégyzet: téglalap és ellipszis kitöltve vagy csak körvonallal.7
Kezelés (30)Mentés PNG fájlba SaveFileDialog-gal, hibakezeléssel.10
Visszavonás: legalább 10 lépés visszamenőleg.10
Az erőforrások felszabadítása (using, Dispose), nincs villogás vagy elcsúszott koordináta.10

Összesen: 100 pont.

📷 Képernyőkép helye

Mit tegyél ide: egy papírra rajzolt terv a programról: felül az eszközsor (eszközök, szín, vastagság, kitöltés, gombok), alatta a nagy fehér rajzfelület.Javasolt fájlnév: winforms-4-5-terv.webp · Alt szöveg: A rajzolóalkalmazás elrendezési terve

Időbeosztás (150 perc)

PercMit csinálj?
0–20Olvasd el az első szakaszt, próbáld ki a példakódot (kék vonal), és tervezd meg az ablakot.
20–40Építsd fel a felületet: PictureBox (SizeMode: Normal, BorderStyle: FixedSingle), eszközök (RadioButton, Appearance: Button), színválasztó, csúszka, jelölőnégyzet, gombok.
40–70Szabadkézi rajz: az egéresemények és a DrawLine, kerek vonalvég, szín és vastagság.
70–105Alakzatok: kezdőpont, előnézet a Paint eseményben, véglegesítés az egér felengedésekor, kitöltés.
105–125Radír, Törlés és Mentés.
125–140Visszavonás.
140–150Tesztlista végigpróbálása és javítás.

Támpontok (kész megoldás nélkül)

Mit tárolj mezőben?
MezőTípusSzerepe
rajzBitmapA vászon, amelyre rajzolsz.
szin, vastagsagColor, intA toll jelenlegi színe és vastagsága.
eszkozstringA kiválasztott eszköz neve ("toll", "vonal", "teglalap", "ellipszis", "radir").
rajzolboolIgaz, amíg az egér gombja le van nyomva.
kezdo, elozo, aktualisPointA húzás kezdőpontja, az utolsó ismert pont (szabadkézi vonalhoz), és az egér jelenlegi helye (előnézethez).
elozmenyekList<Bitmap>A visszavonáshoz elmentett korábbi állapotok.
Egy eseménykezelő az eszközökhöz

Az egyes RadioButton Tag tulajdonságába írd az eszköz nevét a tervezőben (Properties → Tag). Mindegyik CheckedChanged eseménye ugyanazt a metódust hívja. A CheckedChanged kétszer fut (a régi eszköz kikapcsolásakor és az újé bekapcsolásakor), ezért csak akkor tedd el az új eszközt, ha rb.Checked igaz.

Szabadkézi vonal folytonosan

Az egér gyors mozgásánál a MouseMove események között nagy a távolság. Ezért ne pontokat rajzolj, hanem vonalat az előző ponttól az újig (DrawLine(toll, elozo, e.Location)), és az új pont lesz az elozo. A vonalvégeket kerekítsd (StartCap, EndCap = LineCap.Round), hogy vastag vonalnál ne legyen töredezett.

Előnézet az alakzatokhoz

Húzás közben az alakzatot ne a bitmapra rajzold (mert minden egérmozdulat egy újabb alakzatot hagyna), hanem a PictureBox Paint eseményében, az e.Graphics objektummal. Az egérmozgásnál csak az aktualis pontot frissítsd, és hívd az Invalidate() metódust. A bitmapra csak az egér felengedésekor (MouseUp) rajzold az alakzatot.

Téglalap fordított irányú húzásnál

Ha az egér balra vagy felfelé húzol a kezdőponttól, a szélesség vagy a magasság negatív lenne, és a DrawRectangle hibát ad. Készíts egy segédmetódust, amely két pontból szabályos Rectangle-t számol: a bal felső sarok a koordináták minimuma, a méret a különbségek abszolút értéke (Math.Min, Math.Abs).

Visszavonás

Minden új művelet (egér lenyomása) előtt tegyél egy másolatot a bitmapról egy listába ((Bitmap)rajz.Clone()). Ha a lista 10-nél hosszabb, dobd el a legrégebbit (Dispose és RemoveAt(0)). Visszavonáskor az utolsó másolat lesz az új rajz, és a PictureBox Image tulajdonságát is át kell állítani. A régi bitmapot szabadítsd fel.

Mintamegoldás

Előbb oldd meg önállóan!

A mintamegoldás csak az ellenőrzésre való, és nem az egyetlen helyes megoldás. Előbb próbálkozz saját kóddal, és csak elakadásnál vagy a végén nyisd meg.

Megoldás: a felület és a nevek
Vezérlő(Name)Beállítás
FormForm1FormBorderStyle: FixedSingle; MaximizeBox: False; Text: Rajzoló
PictureBoxpbRajzSize: 760 × 440; SizeMode: Normal; BorderStyle: FixedSingle; BackColor: White
RadioButton ×5rbToll, rbVonal, rbTeglalap, rbEllipszis, rbRadirAppearance: Button; Tag: toll, vonal, teglalap, ellipszis, radir; rbToll: Checked; mind a Eszkoz_CheckedChanged metódust hívja
Button, PanelbtnSzin, pnlSzinpnlSzin: BackColor: Black, BorderStyle: FixedSingle
TrackBar, LabeltbVastagsag, lblVastagsagMinimum: 1; Maximum: 30; Value: 3
CheckBoxchkKitoltText: Kitöltés
Button ×3btnTorles, btnVissza, btnMentesText: Törlés, Visszavonás, Mentés
ColorDialog, SaveFileDialogcdSzin, sfdMentessfdMentes: Filter: PNG kép (*.png)|*.png
Megoldás: Form1.cs (teljes kód)
Form1.csC#
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Windows.Forms;

namespace Rajzolo
{
public partial class Form1 : Form
{
private Bitmap rajz;
private Color szin = Color.Black;
private int vastagsag = 3;
private string eszkoz = "toll";
private bool rajzol = false;
private Point kezdo;
private Point elozo;
private Point aktualis;
private readonly List<Bitmap> elozmenyek = new List<Bitmap>();

public Form1()
{
InitializeComponent();
}

private void Form1_Load(object sender, EventArgs e)
{
rajz = new Bitmap(pbRajz.Width, pbRajz.Height);
VaszonTorles();
pbRajz.Image = rajz;
lblVastagsag.Text = "Vastagság: " + vastagsag;
}

private void VaszonTorles()
{
using (Graphics g = Graphics.FromImage(rajz))
{
g.Clear(Color.White);
}
}

// ---- segédmetódusok ----

private static Rectangle Teglalap(Point a, Point b)
{
return new Rectangle(Math.Min(a.X, b.X), Math.Min(a.Y, b.Y), Math.Abs(a.X - b.X), Math.Abs(a.Y - b.Y));
}

private void Alakzat(Graphics g, Point a, Point b)
{
g.SmoothingMode = SmoothingMode.AntiAlias;

using (Pen toll = new Pen(szin, vastagsag))
{
if (eszkoz == "vonal")
{
toll.StartCap = LineCap.Round;
toll.EndCap = LineCap.Round;
g.DrawLine(toll, a, b);
}
else if (eszkoz == "teglalap")
{
Rectangle r = Teglalap(a, b);

if (chkKitolt.Checked)
{
using (SolidBrush ecset = new SolidBrush(szin))
{
g.FillRectangle(ecset, r);
}
}
else
{
g.DrawRectangle(toll, r);
}
}
else if (eszkoz == "ellipszis")
{
Rectangle r = Teglalap(a, b);

if (chkKitolt.Checked)
{
using (SolidBrush ecset = new SolidBrush(szin))
{
g.FillEllipse(ecset, r);
}
}
else
{
g.DrawEllipse(toll, r);
}
}
}
}

private bool AlakzatEszkoz()
{
return eszkoz == "vonal" || eszkoz == "teglalap" || eszkoz == "ellipszis";
}

private void ElozmenyMentese()
{
elozmenyek.Add((Bitmap)rajz.Clone());

if (elozmenyek.Count > 10)
{
elozmenyek[0].Dispose();
elozmenyek.RemoveAt(0);
}
}

// ---- eszközök és beállítások ----

private void Eszkoz_CheckedChanged(object sender, EventArgs e)
{
RadioButton rb = (RadioButton)sender;

if (rb.Checked)
{
eszkoz = rb.Tag.ToString();
}
}

private void btnSzin_Click(object sender, EventArgs e)
{
cdSzin.Color = szin;

if (cdSzin.ShowDialog() == DialogResult.OK)
{
szin = cdSzin.Color;
pnlSzin.BackColor = szin;
}
}

private void tbVastagsag_Scroll(object sender, EventArgs e)
{
vastagsag = tbVastagsag.Value;
lblVastagsag.Text = "Vastagság: " + vastagsag;
}

// ---- egér ----

private void pbRajz_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left)
{
return;
}

ElozmenyMentese();
rajzol = true;
kezdo = e.Location;
elozo = e.Location;
aktualis = e.Location;
}

private void pbRajz_MouseMove(object sender, MouseEventArgs e)
{
if (!rajzol)
{
return;
}

aktualis = e.Location;

if (AlakzatEszkoz())
{
pbRajz.Invalidate(); // az előnézetet a Paint rajzolja
return;
}

Color tollSzin = eszkoz == "radir" ? Color.White : szin;

using (Graphics g = Graphics.FromImage(rajz))
using (Pen toll = new Pen(tollSzin, vastagsag))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
toll.StartCap = LineCap.Round;
toll.EndCap = LineCap.Round;
g.DrawLine(toll, elozo, e.Location);
}

elozo = e.Location;
pbRajz.Invalidate();
}

private void pbRajz_MouseUp(object sender, MouseEventArgs e)
{
if (!rajzol)
{
return;
}

rajzol = false;

if (AlakzatEszkoz())
{
using (Graphics g = Graphics.FromImage(rajz))
{
Alakzat(g, kezdo, e.Location);
}
}

pbRajz.Invalidate();
}

private void pbRajz_Paint(object sender, PaintEventArgs e)
{
if (rajzol && AlakzatEszkoz())
{
Alakzat(e.Graphics, kezdo, aktualis); // előnézet, nem kerül a bitmapra
}
}

// ---- gombok ----

private void btnTorles_Click(object sender, EventArgs e)
{
ElozmenyMentese();
VaszonTorles();
pbRajz.Invalidate();
}

private void btnVissza_Click(object sender, EventArgs e)
{
if (elozmenyek.Count == 0)
{
return;
}

Bitmap utolso = elozmenyek[elozmenyek.Count - 1];
elozmenyek.RemoveAt(elozmenyek.Count - 1);

Bitmap regi = rajz;
rajz = utolso;
pbRajz.Image = rajz;
regi.Dispose();
}

private void btnMentes_Click(object sender, EventArgs e)
{
sfdMentes.FileName = "rajz";

if (sfdMentes.ShowDialog() != DialogResult.OK)
{
return;
}

try
{
rajz.Save(sfdMentes.FileName, ImageFormat.Png);
MessageBox.Show("A rajz mentve.");
}
catch (Exception ex)
{
MessageBox.Show("A mentés nem sikerült: " + ex.Message);
}
}

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
foreach (Bitmap b in elozmenyek)
{
b.Dispose();
}

rajz.Dispose();
}
}
}
Megoldás: miért így épül fel?

A tartós rajz a rajz bitmapon él, az átmeneti előnézet a Paint eseményben jelenik meg, és nem kerül a bitmapra. A szabadkézi vonal minden MouseMove eseménynél az előző pontot köti össze az újjal, ezért folytonos. Az alakzatokat az egér felengedésekor rögzítjük. A Teglalap segédmetódus a fordított húzást is kezeli. A visszavonás a Clone() által készített másolatokat használja, a régi bitmapot pedig felszabadítja. A Graphics, a Pen és a SolidBrush mindig using blokkban él.

Tesztlista

#TesztVárt eredmény
1A program indul.Fehér rajzfelület, a toll az alapértelmezett eszköz.
2Szabadkézi rajz lassan és nagyon gyorsan.Folytonos vonal, nincsenek szakadások.
3Szín- és vastagságváltoztatás.Az új vonal az új színnel és vastagsággal jelenik meg, a korábbi vonalak nem változnak.
4Vonal húzása.Húzás közben előnézet látszik, felengedéskor a vonal a rajzon marad.
5Téglalap húzása jobbra-le, majd balra-fel.Mindkét irányban hibátlan, nincs kivétel.
6Ellipszis kitöltéssel és anélkül.A jelölőnégyzet szerint kitöltött vagy körvonalas.
7Radír.A rajz fehérre törlődik azon a helyen.
8Az ablak lekicsinyítése és visszanagyítása.A rajz megmarad.
9Visszavonás 3-szor, majd 11 lépés után is.A rajz lépésenként visszalép; 10 lépésnél nem megy tovább, nincs hiba.
10Törlés, majd visszavonás.A törölt rajz visszajön.
11Mentés PNG fájlba, majd megnyitás képnézegetővel.A kép megegyezik a rajzzal, a háttér fehér.
12Mentés nem létező mappába vagy írásvédett helyre.Érthető hibaüzenet, a program nem omlik össze.
13Jobb egérgomb a rajzfelületen.Nem rajzol, nem hiba.
14Egér kihúzása a rajzfelületről húzás közben.A program nem omlik össze.

Gyakori hibák és önértékelés

Ezeken szoktak elcsúszni

Gondold végig, nálad előfordul-e valamelyik.

1. A rajz eltűnik, ha az ablakot eltakarják

Graphics g = pbRajz.CreateGraphics();
g.DrawLine(toll, elozo, e.Location);
Hiba és javítás

A CreateGraphics() közvetlenül a vezérlő felületére rajzol, amelyet a rendszer újrafest, és a rajz elveszik. Rajzolj a Bitmap-ra (Graphics.FromImage), a PictureBox pedig jelenítse meg a képet.

2. A vonal eltér az egér helyétől

A rajzolt vonal nem ott jelenik meg, ahol az egér van.

Ok és javítás

A PictureBox SizeMode tulajdonsága Zoom vagy StretchImage, ezért a kép méretezve látszik, és az egér koordinátája nem egyezik a bitmap koordinátájával. Állítsd Normal értékre, és a bitmap mérete egyezzen a PictureBox méretével.

3. Kivétel a téglalap húzásakor

g.DrawRectangle(toll, kezdo.X, kezdo.Y, aktualis.X - kezdo.X, aktualis.Y - kezdo.Y);
Hiba és javítás

Ha az egér balra vagy felfelé húzol, a szélesség vagy a magasság negatív, és a rajzolás hibát jelez, vagy semmit sem rajzol. Számold ki a szabályos téglalapot: bal felső sarok = a koordináták minimuma, méret = a különbségek abszolút értéke.

4. Fogy a memória, a program lassul

Hosszabb rajzolás után a program lassú, és a Feladatkezelőben nő a memóriahasználata.

Ok és javítás

A Pen, a Graphics és a Bitmap objektumokat nem szabadítottad fel. Használj using blokkot, és a visszavonás régi bitmapjait, valamint az ablak bezárásakor az összes bitmapot dobd el (Dispose).

5. Az eszköz kétszer vált

Eszközváltáskor a program néha a régi eszközt használja.

Ok és javítás

A RadioButton.CheckedChanged esemény kétszer fut: az egyik gombnál kikapcsolásakor, a másiknál bekapcsolásakor. Ha nem vizsgálod a Checked értéket, a kikapcsolt gomb is beállítja az eszközt, és az utolsó futás dönt. Ellenőrizd: if (rb.Checked).

6. A mentés GDI+ hibát ad

// ExternalException: A generic error occurred in GDI+.
rajz.Save(utvonal, ImageFormat.Png);
Hiba és javítás

Ez az általános GDI+ hiba gyakran azt jelenti, hogy az útvonal érvénytelen vagy nem írható (nincs jogosultság, nem létező mappa), vagy a fájlt egy másik program használja. Védd try–catch blokkal, és adj érthető üzenetet. Ha a képet ugyanabból a fájlból töltötted be, amelybe menteni akarsz, a betöltő bitmap zárolja a fájlt: új fájlnévre ments.

Az értékeléshez ellenőrizd az alábbi szempontokat is:

MűködésA tesztlista mind a 14 pontja teljesül.
ErőforrásokMinden Graphics, Pen, Brush és Bitmap felszabadul.
SzerkezetAz alakzatrajzolás egyetlen metódusban van, amelyet az előnézet és a véglegesítés is hív.
HasználhatóságAz eszköz, a szín és a vastagság állapota látszik, a visszavonás működik.

Bővítési ötletek, ha kész vagy

Ha maradt időd, próbáld ki:

  • Pipetta: egy eszköz, amely a rajzon kattintott pont színét átveszi (GetPixel).
  • Szaggatott vonal: a Pen.DashStyle tulajdonsággal, legördülő listából.
  • Tükrözés: a szabadkézi vonal tükörképét is megrajzolod a vászon függőleges tengelye mentén.
  • Kép megnyitása: egy meglévő képre is rajzolhass (OpenFileDialog).
  • Nagyítás: a kép nagyítása a TrackBar segítségével (haladó: a koordináták átszámolásával).

Gyakorlótár: 6 további feladat

Három szinten gyakorolhatsz: alap, közepes és kihívás. A feladatokhoz a kész rajzoló szükséges. Csak a próbálkozás után nyisd le a megoldást.

Alap szint

1. Színek gyorsgombjai

Három kis Panel (piros, kék, zöld) felett kattintva a rajzolószín az adott szín legyen, és a pnlSzin mutassa az új színt. Hogyan oldod meg három külön kód nélkül?

Megoldás megnyitása

A három panel Click eseménye ugyanazt a metódust hívja. A sender a megkattintott panel, a BackColor tulajdonsága adja a színt.

Form1.csC#
private void GyorsSzin_Click(object sender, EventArgs e)
{
Panel panel = (Panel)sender;
szin = panel.BackColor;
pnlSzin.BackColor = szin;
}

2. A rajzfelület háttérszíne

Egy Háttér gomb színválasztás után fesse be a teljes rajzfelületet a kiválasztott színnel (az eddigi rajz elvész, de visszavonható).

Megoldás megnyitása

A Graphics.Clear(szín) az egész felületet az adott színnel tölti ki. A művelet előtt elmentjük az állapotot, hogy visszavonható legyen. A radír színét is érdemes ehhez igazítani (egy hatterSzin mezőben tárolva).

Form1.csC#
private Color hatterSzin = Color.White;

private void btnHatter_Click(object sender, EventArgs e)
{
if (cdSzin.ShowDialog() != DialogResult.OK)
{
return;
}

ElozmenyMentese();
hatterSzin = cdSzin.Color;

using (Graphics g = Graphics.FromImage(rajz))
{
g.Clear(hatterSzin);
}

pbRajz.Invalidate();
}

// a radírnál: Color tollSzin = eszkoz == "radir" ? hatterSzin : szin;

Közepes szint

3. Pipetta

Egy Pipetta eszközzel (rbPipetta, Tag: pipetta) egy kattintással a rajzon lévő pont színét vedd át aktuális színnek, majd térj vissza a tollhoz.

Megoldás megnyitása

A Bitmap.GetPixel(x, y) egy képpont színét adja. A koordináta a bitmapon belül kell legyen, ezért ellenőrizzük. A pipettával kattintva nem kezdünk rajzolást: a MouseDown elején kezeljük, és kilépünk.

Form1.cs (a MouseDown elején)C#
if (eszkoz == "pipetta")
{
if (e.X >= 0 && e.Y >= 0 && e.X < rajz.Width && e.Y < rajz.Height)
{
szin = rajz.GetPixel(e.X, e.Y);
pnlSzin.BackColor = szin;
}

rbToll.Checked = true; // vissza a tollhoz
return;
}

4. Tükrözés

Egy chkTukor jelölőnégyzet bekapcsolt állapotában a szabadkézi vonal tükörképe is megjelenik a vászon függőleges tengelye mentén (mandala-hatás).

Megoldás megnyitása

A tükörkép x koordinátája szélesség - 1 - x, az y változatlan. Mindkét szakaszt (az eredetit és a tükörképet) ugyanazzal a tollal rajzoljuk.

Form1.cs (a MouseMove szabadkézi ágában)C#
g.DrawLine(toll, elozo, e.Location);

if (chkTukor.Checked)
{
int w = rajz.Width - 1;
g.DrawLine(toll, new Point(w - elozo.X, elozo.Y), new Point(w - e.X, e.Y));
}

Kihívás szint

5. Vonalstílus

Egy cmbStilus lista (Folytonos, Szaggatott, Pontozott) szabja meg a vonal stílusát a vonal, téglalap és ellipszis eszközöknél (előnézetben is).

Megoldás megnyitása

A Pen.DashStyle tulajdonság felsorolás típusú (DashStyle.Solid, Dash, Dot), a System.Drawing.Drawing2D névtérből. A listaelem sorszámából választjuk ki. Az Alakzat metódus a tollat létrehozza, ezért ott állítjuk be, így az előnézet és a végleges rajz is ugyanazt használja.

Form1.csC#
// az Alakzat metódusban, a Pen létrehozása után:
using (Pen toll = new Pen(szin, vastagsag))
{
if (cmbStilus.SelectedIndex == 1)
{
toll.DashStyle = DashStyle.Dash;
}
else if (cmbStilus.SelectedIndex == 2)
{
toll.DashStyle = DashStyle.Dot;
}

// ... a rajzolás változatlan ...
}

6. Kép megnyitása és rárajzolás

Egy Megnyitás gomb töltsön be egy képfájlt (PNG, JPG) a rajzfelületre (méretezve, a képarányt megtartva), és lehessen rárajzolni. A betöltés után a kép fájlja ne maradjon zárolva.

Megoldás megnyitása

A new Bitmap(fájl) a fájlt addig zárolja, amíg a bitmap él. Ezért a betöltött képet csak átmásoljuk a vászonra (DrawImage), és a betöltött bitmapot a using blokk azonnal felszabadítja. A méretezés a képarányt tartja: a kisebbik skálát (Math.Min) használjuk.

Form1.csC#
private void btnMegnyit_Click(object sender, EventArgs e)
{
ofdKep.Filter = "Képek (*.png;*.jpg;*.jpeg)|*.png;*.jpg;*.jpeg";

if (ofdKep.ShowDialog() != DialogResult.OK)
{
return;
}

try
{
ElozmenyMentese();

using (Bitmap kep = new Bitmap(ofdKep.FileName))
using (Graphics g = Graphics.FromImage(rajz))
{
g.Clear(Color.White);

double arany = Math.Min((double)rajz.Width / kep.Width, (double)rajz.Height / kep.Height);
int w = (int)(kep.Width * arany);
int h = (int)(kep.Height * arany);

g.DrawImage(kep, (rajz.Width - w) / 2, (rajz.Height - h) / 2, w, h);
}

pbRajz.Invalidate();
}
catch (Exception ex)
{
MessageBox.Show("A kép nem nyitható meg: " + ex.Message);
}
}

Gyors önellenőrzés

Válaszolj fejben, és csak utána nyisd le a választ.

1. Miért a Bitmapra rajzolunk, és nem közvetlenül a PictureBoxra?

A bitmap tartalma megmarad. A vezérlő felületére (CreateGraphics) rajzolt tartalom elveszik, amikor a rendszer újrafesti a vezérlőt.

2. Mire való az Invalidate()?

Jelzi, hogy a vezérlő tartalma megváltozott, és újra kell rajzolni. Ekkor fut a Paint esemény, és a PictureBox frissíti a megjelenített képet.

3. Hogyan lesz folytonos a szabadkézi vonal?

Az előző pont és az új pont közé vonalat húzunk (DrawLine), és az új pont lesz az előző. Egyes pontok rajzolása gyors mozgásnál szakadásokat adna.

4. Miért kell az alakzat előnézetét a Paint eseményben rajzolni?

Hogy az előnézet ne kerüljön a bitmapra: minden egérmozdulat felülírná a korábbi előnézetet, és nem maradna szemét a rajzon. Csak az egér felengedésekor kerül az alakzat a bitmapra.

5. Miért kell a téglalapnál a Math.Min és Math.Abs?

Hogy a bal felső sarok és a pozitív méret akkor is helyes legyen, ha az egér balra vagy felfelé húzol a kezdőponttól.

6. Miért kell a Pen és a Graphics felszabadítása?

Operációs rendszer erőforrást foglalnak. A using blokk (Dispose) a végén felszabadítja őket, különben a program memóriát és rajzolási erőforrást veszít.

Mit tudsz már?

  • Bitmapra rajzolsz, és a PictureBoxban megjeleníted.
  • Az egéresemények alapján szabadkézi vonalat és alakzatokat építesz.
  • Előnézetet adsz a Paint eseménnyel.
  • Visszavonást és mentést készítesz, az erőforrásokat helyesen kezeled.
  • Tesztterv alapján ellenőrzöd a munkád.

Következő feladat: címjegyzék vagy teendőlista

A rajzolóprogram kész. A következő önálló feladatban egy adatkezelő programot építesz: egy címjegyzéket vagy egy teendőlistát, adatbázissal vagy fájllal.