Widget-ünket a festők vásznáról Canvas-nak nevezzük el. Mit kell a widgetnek tudnia?

Hasonló dokumentumok
bool _freehand = false; QPoint _lastpoint; // ebben a pontban volt az utolsó rajzolásnál az egérmutató

A jobboldalon a pnlright egy Stacked Widget Állítsuk be az első lapot és nevezzük el pnldraw-ra:

if(_param.antialias) painter.setrenderhint(qpainter::antialiasing, true);

Objektumok és osztályok. Az objektumorientált programozás alapjai. Rajzolás tollal, festés ecsettel. A koordinátarendszer

A Paint program használata

Számítástechnika II. BMEKOKAA Előadás. Dr. Bécsi Tamás

A Vonallánc készlet parancsai lehetővé teszik vonalláncok és sokszögek rajzolását.

Johanyák Zsolt Csaba: Ugráló gomb oktatási segédlet Copyright 2008 Johanyák Zsolt Csaba

components : IContainer dx : int dy : int tmidőzítő : Timer toolstripseparator1 : ToolStripSeparator tsmikilépés : ToolStripMenuItem

3. Osztályok II. Programozás II

Java és web programozás

Bevezetés a programozásba II 1. gyakorlat. A grafikus könyvtár használata, alakzatok rajzolása

Grafikus felületek a programozó szempontjából grafikus elemek absztrakt reprezentációja az egyes elemek tulajdonságait leíró adatstruktúrák.

Objektumorientált programozás Pál László. Sapientia EMTE, Csíkszereda, 2014/2015

Imagine Logo Tanmenet évfolyam

VII. Appletek, grafika

Programozás BMEKOKAA146. Dr. Bécsi Tamás 8. előadás

Programozási nyelvek 1. előadás

Elemi grafika. Stílusok, időzítő, képek

Képfájlok beolvasása és megjelenítése

Programozás II gyakorlat. 6. Polimorfizmus

Mesh generálás. IványiPéter

CAD-ART Kft Budapest, Fehérvári út 35.

Mechatronika segédlet 3. gyakorlat

QGIS szerkesztések ( verzió) Összeállította: dr. Siki Zoltán verzióra aktualizálta: Jáky András

Számítástechnika II. BMEKOKAA Előadás. Dr. Bécsi Tamás

Mechatronika segédlet 1. gyakorlat

Grafikus Qt programok írása segédeszközök nélkül

BME MOGI Gépészeti informatika 15.

Osztályok. 4. gyakorlat

Word VI. Lábjegyzet. Ebben a részben megadhatjuk, hogy hol szerepeljen a lábjegyzet (oldal alján, szöveg alatt).

Eseményvezérelt alkalmazások fejlesztése II 3. előadás. Windows Forms dinamikus felhasználói felület, elemi grafika

Információ megjelenítés Számítógépes ábrázolás. Dr. Iványi Péter

Eseményvezérelt alkalmazások fejlesztése I 4. előadás. Elemi grafika és egérkezelés

Függvények Függvények

Ugráló gomb oktatási segédlet Ugráló gomb

C programozási nyelv Pointerek, tömbök, pointer aritmetika

Képszerkesztés. Letölthető mintafeladatok gyakorláshoz: Minta teszt 1 Minta teszt 2. A modul célja

Név Magasság Szintmagasság tető 2,700 koszorú 0,300 térdfal 1,000 födém 0,300 Fsz. alaprajz 2,700 Alap -0,800

BME MOGI Gépészeti informatika 7.

Programozási nyelvek 2. előadás

Programozási technikák Pál László. Sapientia EMTE, Csíkszereda, 2009/2010

BME MOGI Gépészeti informatika 18. Grafika, fájlkezelés gyakorló óra. 1. feladat Készítsen alkalmazást az = +

Közös keret egy objektumhalmazra

Qt rajzolás munkafüzet. Elemi Alkalmazások fejlesztése 3.

Területi primitívek: Zárt görbék által határolt területek (pl. kör, ellipszis, poligon) b) Minden belső pont kirajzolásával (kitöltött)

Téglalap kijelölés opciói

Lakóház tervezés ADT 3.3-al. Segédlet

KÉPERNYŐKÉP KÉSZÍTÉSE

QGIS tanfolyam (ver.2.0)

Egyéb 2D eszközök. Kitöltés. 5. gyakorlat. Kitöltés, Szöveg, Kép

1.A. feladat: Programablakok

Elemi alkalmazások fejlesztése III.

Osztály és objektum fogalma

Programozás C++ -ban

Bevezetés a programozásba előadás: Öröklődés

Táblázatok. Táblázatok beszúrása. Cellák kijelölése

3D-s számítógépes geometria és alakzatrekonstrukció

Photofiltre használata KAP képek javításához

BME MOGI Gépészeti informatika 6.

EDInet Connector telepítési segédlet

Táblázatkezelés 2. - Adatbevitel, szerkesztés, formázás ADATBEVITEL. a., Begépelés

Programozás C++ -ban

Prezentáció, Diagramok, rajzolt objektumok. Szervezeti diagram

Programozás C és C++ -ban

Alkalmazott modul: Programozás

8. gyakorlat Pointerek, dinamikus memóriakezelés

Programozás. (GKxB_INTM021) Dr. Hatwágner F. Miklós április 4. Széchenyi István Egyetem, Gy r

Eseményvezérelt alkalmazások fejlesztése I 4. előadás. Elemi grafika és egérkezelés. Elemi grafika és egérkezelés Rajzolás grafikus felületen

1. Template (sablon) 1.1. Függvénysablon Függvénysablon példányosítás Osztálysablon

Autodesk Inventor Professional New Default Standard.ipt

Programozási alapismeretek 4.

OOP #14 (referencia-elv)

16/8/4 CSATORNÁS Real Time MPEG-4DVR. 16/8/4 CSATORNÁS beépített DVD-RW íróval vagy CD-RW íróval

Form1 Form Size 400;400 Text Mozgó kör timer1 Timer Enabled True Interval 100

és az instanceof operátor

Választó lekérdezés létrehozása

Közegek és felületek megadása

A program a köröket és köríveket az óramutató járásával ellentétes irányban rajzolja meg.

C programozási nyelv

BME MOGI Gépészeti informatika 14.

Rajz 02 gyakorló feladat

Java VIII. Az interfacei. és az instanceof operátor. Az interfészről általában. Interfészek JAVA-ban. Krizsán Zoltán

1. Mi a fejállományok szerepe C és C++ nyelvben és hogyan használjuk őket? 2. Milyen alapvető változókat használhatunk a C és C++ nyelvben?

Rajz 01 gyakorló feladat

Programozás II. 2. Dr. Iványi Péter

3D-s számítógépes geometria és alakzatrekonstrukció

A program telepítése

Programozás 7.o Az algoritmus fogalma (ismétlés)

1.1.1 Dátum és idő függvények

3. Ezután a jobb oldali képernyő részen megjelenik az adatbázistábla, melynek először a rövid nevét adjuk meg, pl.: demo_tabla

Az osztályok csomagokba vannak rendezve, minden csomag tetszőleges. Könyvtárhierarhiát fed: Pl.: java/util/scanner.java

Diagram formázása. A diagram címének, a tengelyek feliratainak, jelmagyarázatának, adatfeliratainak formázása

Tömörítés, csomagolás, kicsomagolás. Letöltve: lenartpeter.uw.hu

Word V. tabulátortípus meg nem jelenik: Tabulátor - balra, Tabulátor - jobbra,

ESEMÉNY VEZÉRELT ALKALMAZÁSOK FEJLESZTÉSE I. Bevezetés. Készítette: Gregorics Tibor

Baran Ágnes. Gyakorlat Függvények, Matlab alapok

QT Grafika az alap alkalmazás (GrafikaQtvel) működése

KidPad 1.0. Felhasználói kézikönyv

Átírás:

A Canvas widget Widget-ünket a festők vásznáról Canvas-nak nevezzük el. Mit kell a widgetnek tudnia? 1. Más widgetekre rárakható legyen, 2. Ha a befoglaló (szülő) widget mérete változik az ő mérete is változzon 3. Mindeközben a rárajzolt kép ne tűnjön el 4. Maradjon meg a tartalma akkor is, ha az őt tartalmazó szülő widgetet lekicsinyítjük, vagy elrejtjük, 5. Tartalmát kitörölhessük. 6. Bármikor rajzolhassunk rá, nem csak egy paintevent()-ben 7. Rajzolhassunk rá az egérrel, tehát kezelje le a megfelelő egér eseményeket 8. Tudjon egyenes és görbe vonalat húzni, 9. Ismerjen néhány egyszerű alakzatot (háromszög, négyszög, ellipszis), 10. Ezeket különböző mintázatokkal ki lehessen tölteni. 11. Képes legyen képek és fotók megjelenítésére, nagyítására, kicsinyítésére, forgatására, stb. 12. Legyen képes képeken különböző szűréseket végezni. 13. Ki lehessen nyomtatni a képet, amit rárajzoltunk, illetve beolvastunk. Ez a lista még bővíthető. Nyilvánvaló, hogy mindez a funkcionalítás csak egy viszonylag bonyolult programmal valósítható meg. Ezért mi lépésenként haladunk: először a rajzolással foglalkozunk csak, a képfeldolgozást később adjuk hozzá. A rajzolás Hagyományos nem esemény vezérelt programokban bármikor rajzolhatunk a képernyőre, és a rajzolás eredménye azonnal meg is jelenik. Esemény vezérelt programokban azonban a képernyőre csak a kirajzolási eseményt kezelő függvényben lehet rajzolni. Magát a rajzolást a QPainter osztály végzi, ami bármilyen QPaintDevice-ból leszármaztatott objektumra tud rajzolni. A QPaintDevice egy absztrakt 2D rajzolási felület, amelynek origója a bal felső sarkában van, az x tengelye vízszintesen jobbra, az y tengelye lefelé mutat és a

rajzolási egysége a pixel. A következőek 1 belőle leszármaztatott osztályok: QWidget, QImage, QPixmap, QBitmap, QPicture, és QPrinter. A QImage képek beolvasására, kiírására és pixeleinek módosítására optimalizált,osztály A Qimage tartalma eszközfüggetlen. Egyszínű (monochrome), 8 és 32 bites, illetve alfa csatornával összeolvasztott képeket egyaránt kezel. A képformátumok közül az alábbiakat mind ismeri: BMP Windows Bitmap Read/write GIF Graphic Interchange Format (optional) Read JPG Joint Photographic Experts Group JPEG Joint Photographic Experts Group PNG Portable Network Graphics PBM Portable Bitmap PGM Portable Graymap PPM Portable Pixmap TIFF Tagged Image File Format XBM X11 Bitmap XPM X11 Pixmap Read/write Read/write Read/write Read Read Read/write Read/write Read/write Read/write A QPixmap a képek kirajzolására optimalizált,osztály. A QBitmap egy csupán 1 és 0 pixel értékeket tartalmazó képet tárolhat ezt pl. ecsetek definiálására használhatjuk, a QPicture viszont a rajzolási parancsok megjegyzésére, eltárolására (metafile-ban) és visszajátszására szolgál. A QPainter tehát közvetlenül rajzolhat ezekre. egyszerű egyenes szakaszokat és összetett alakzatokat egyaránt. A következő ún. alap rajzelemeket (drawing primitives) ismeri: pont, vonal, téglalap, kerekített sarkú téglalap, ellipszis, ív, kördiagram, ellipsziscikk/körcikk, töröttvonal, sokszög, convex sokszög és Bezier spline görbe ( drawpoint(), drawpoints(), drawline(), drawrect(), drawroundedrect(), drawellipse(), drawarc(), drawpie(), drawchord(), drawpolyline(), drawpolygon(), drawconvexpolygon() és a drawcubicbezier() ). Tud ezenkívül az aktuális ecsettel kitöltött utakat (path) is kezelni, vagy képeket kirajzolni. 1 Más, itt fel nem sorolt, osztályok is vannak.

Ha közvetlenül rajzolunk egy widget-re, akkor azt kizárólag a widget paintevent(qpaintevent *) függvényében 2, vagy egy abból hívott függvényben szabad ezt megtegyük. Ebből az is következik, hogy egy QWidget objektumra nem, csak olyan widget-ekre tudunk rajzolni, amelyeket a QWidgetből leszármaztattunk, hiszen a QWidget paintevent()függvényébe nem tudunk belenyúlni. Miért ez a megkötés? Amikor egy widget-et (pl. egy ablakot) miniatürizálunk, vagy azt egy másik widget teljesen vagy részlegesen eltakarja, akkor annak tartalmát az ablakkezelő rendszer többnyire nem menti el. Amikor a widget eltakart része újra láthatóvá válik, akkor annak tartalmát ismét ki kell rajzolni, ennek érdekében a paintevent() függvény meghívódik 3, így a widget tartalma visszaállítható. A widgetek azon részeit, amiket nem mi rajzolunk ki (pl. egy címke szövegét) ugyanekkor a Qt maga rajzolja újra. Kétféle módszert használhatunk ábrák és képek kirajzolására Az első az, amikor az ábrát, ill képet az elemi rajzolási műveletekből újra és újra előállítjuk. Legegyszerűbben azonban úgy járhatunk el, ha nem közvetlenül a widget-re, hanem egy háttértárolóra rajzolunk. Háttértárolóként egy másik QPaintDevice-ot, pl QImage-t, vagy QPixmap-et használunk, arra bármikor rajzolhatunk és a paintevent()-ben azt rajzoltatjuk ki a widgetre. Ez a módszer a kettős tárolás (Double Buffering). A Canvas egy ilyen alapon működő widget. Háttértárolójaként egy _canvas nevű QImage-t használunk. 2 Emlékeztetünk, hogy a Qt ben az egyes widgetek közötti kommunikáció SIGNAL-SLOT mechanizmusa mellett eseménykezelés is van. Az események az operációs rendszerből származnak, pl amikor a felhasználó mozgatja az egeret, vagy valamilyen widgetnek ki kell magát rajzolnia, akkor az operációs rendszer küld egy üzenetet a Qt alkalmazásnak, ami ennek hatására meghívja valamelyik widget eseménykezelő függvényét. 3 A paintevent() függvény azonban akkor hívódik meg, ha már más esemény nem várakozik az alkalmazás üzenet ciklusában (Message Loop).

Általános megjegyzések a) Minden rajzolást a _canvas-ra végzünk. Ahhoz, hogy ezt megtehessük egy minimum akkora QImage-re van szükségünk, int a widgetünk mérete. Amikor a widget mérete megváltozik a _canvas méretét is meg kell változtassuk. Mivel azt akarjuk, hogy a rárajzolt kp akkor is megmaradjon, ha a widget-et összenyomjuk, majd újra megnagyítjuk a _canvas méretét csak növelnünk szabad. Ehhez a nyilvános (public) Resize() függvényt használjuk: // Bármilyen képre jó, ha ez a _canvas, akkor a szülő méretére kell állítani! // Csak nagyobb newsize esetén változtatja meg a méretet void Resize(QImage *image, QSize newsize); Azért választottunk egy nyilvános függvényt, aminek a QImage a paramétere, hogy ne csak a háttértárolóra, hanem tetszőleges más képre is használható legyen. Ezt a függvényt a Canvas widget resizeevent()-jében használjuk majd. Ez az eseménykezelő a widget megjelenítése előtt már meghívódik, ezért a Canvas konstruktorában nem kell használnunk. b) A háttártároló tartalma nem jelenik meg automatikusan a Canvas widget-en. Azt a paintevent()-ben tudjuk csak kirajzolni. A paintevent létrehozására a widgetek update()függvényeinek valamelyikét használjuk, ami a rajzolások után egy Paint Event-et helyez el a widget esemény ciklusába. c) Kitöltött alakzatok rajzolásához csak annyit kell tennünk, hogy a QPainter ecsetjét beállítjuk a rajzolás előtt.

A Canvas widget létrehozása és a projekthez adása Először is adjunk hozzá a projekthez egy új fájlt a canvas.h-t! Windows Visual Studio + Qt A Visual Studio alatt:a Solution Explorer-ben kattintsunk a jobb egérgombbal a projekt nevére és válasszuk az Add /New Item menüt! Kattintsunk a Header File sorra, írjuk be a fájl nevét, majd Rendben/Ok. Ugyanígy hozzuk majd létre a canvas.cpp fájlt is, de most foglalkozzunk a header-rel. Windows és Linux Qt creator Qt Creator-ban az Add New menüben felugró ablakban válasszuk ki a C++-t a bal oldalon, a C++ Header File-t a jobb oldalon, tovább lépve adjuk meg a fájl nevét, utána hagyjuk az alapértelmezést meg és kattintsunk a Finish-re! A kétféle módon előállított header fájl különbözik: a Visual Studio-beli egy sort, #pragma once a Qt Creatorral létrehozott a #ifndef CANVAS_H #define CANVAS_H #endif // CANVAS_H sorokat tartalmazza. Mindkettő jó akár windows, akár Linux alatt 4. Ebbe a fájlba (ha van, akkor a #endif elé) először include-oljuk be a szükséges header-eket: #include <QColor> // a színek megadásához #include <QImage> // a rajz tárolására #include <QWidget> // minden widget-hez kell #include <QPainter> // a QPainter rajzol #include <QPaintEvent> // widgetre csak a Paint Event-ben rajzolhatunk #include <QMouseEvent> // egér események (gombnyomás, felengedés, mozgatás) #include <QResizeEvent> // átméretezési esemény Mindenekelőtt deklaráljunk egy osztályt a rajzolásokhoz használt globális paraméterek tárolására: struct RajzParameterek 4 A #pragma once gyorsabb, mert a fordítónak nem kell megkeresnie a #ifdef-et lezáró #endif-et. A másik viszont lehetőséget ad arra, hogy megnézzük egy benne levő osztály, vagy változó definiálva van e.

; int penwidth = 1; Qt::PenStyle penstyle = Qt::SolidLine; Qt::PenCapStyle pencap = Qt::FlatCap; Qt::PenJoinStyle penjoin = Qt::MiterJoin; Qt::BrushStyle brushstyle = Qt::SolidPattern; QColor pencolor = qrgb(0, 0, 0), brushcolor = qrgb(255, 266, 255); Itt egyúttal kezdőértékeket is beállítunk (ez csak a C++11-ben működik). Ezt az osztályt felhasználjuk a Canvas-t használó más programokban, most pl a grafikaqtvel.cpp-ben. Itt hívjuk fel a figyelmet arra, hogy noha bármilyen ecset stílust beállíthatunk, a kirajzolásnál az átmeneteket tartalmazó (gradient) stílusok használata bonyolultabb, mint amit itt kezelünk majd, így azok nem fognak működni! Ezután deklaráljuk az új widget-et: class Canvas : public QWidget Az első sor mint mindig: Q_OBJECT Kényelmi okokból magában a Canvas-ban is tároljuk el lokálisan a globális paramétereket: RajzParameterek _param; Állítsunk be néhány jelzést (A C++11-től kezdve ezt a deklarációban is megtehetjük, nem kell a konstruktorba beírni): bool _modified = false; // nem kell elmenteni a képet bool _drawing = false; // nem rajzolás közben vagyunk bool _freehand = false; // nem szabadkézi rajzolás közben vagyunk Jegyezzük meg az utoljára használt pont helyét: QPoint _lastpoint; // ebben a pontban volt az utolsó rajzolásnál az egérmutató A QPoint egy látszólag felesleges osztály, hiszen csak az egész típusú x és y koordinátákat tartalmazza, ráadásul ezek kiolvasása és módosítása függvényhívásokat igényel. Kiolvasás: x() és y(), ami az értéket adja vissza és rx(), ry(), amelyek referenciát adnak a belső változókra. A módosítás a setx() és sety(), illetve az rx() és ry() függvényekkel történhet 5. Pl. QPoint p; p.setx(p.x() + 1); p.ry() = p.y() + 3; p.rx()++; 5 Ez nem lassítja le a programot, mert a fordító ezeket a függvényeket az aktuális változókkal helyettesíti

Ami miatt sokszor azért is kényelmes használni QPoint-ot az az, hogy úgy viselkedik, mint egy origóból húzott vektor, vagyis két QPoint koordinátánként adódik össze és számmal szorozva, ill. osztva mindkét koordináta szorzódik, ill. osztódik. Az isnull() függvénnyel megnézhetjük azt is, hogy nem null vektort adunk e meg, sőt a vektor hosszát 6 (manhattenlength() ) )is lekérhetjük. Természetesen egy QPoint egy másik QPoint-tal egyenlővé is tehető. Használhatjuk alakzatok elmozgatásánál az elmozdulás vektor megadására is. Egy hasonló, de valós koordinátákat használó osztály a QPointF. A tényleges rajzolási területünk, ahova bármikor rajzolhatunk, nem csak egy kirajzolási eseményben (Paint Event-ben) egy QImage: QImage _canvas; // ide rajzolunk A konstruktor és a destruktor a nyilvános részbe kerül: public: Canvas(QWidget *parent = 0); virtual ~Canvas() A virtual kulcsszó azt jelenti, hogy az illető osztályból leszármaztatott osztályokban majd átdefiniálhatjuk ezt. Itt most felesleges, de szokjunk hozzá, hogy megadjuk! A rajzolási paramétereket akár futás közben is megváltoztathatjuk a void SetParameters(RajzParameterek &param) _param = param; függvénnyel. A rajzterület méretét dinamikusan tudjuk változtatni a painteven() eseményben. Ezzel elérhetjük, hogy sohase kelljen feleslegesen memóriát lefoglalva tartani. Erre szolgál a void Resize(QImage *image, QSize newsize); függvény. Adatok lekérdezése: bool IsModified() const return _modified; bool IsDrawing() const return _drawing; bool IsModified() const return _modified; QColor GetPixel(const QPoint &pt); QColor PenColor() const return _pencolor; int PenWidth() const return _penwidth; Qt::PenStyle PenStyle() const return _penstyle; Qt::PenCapStyle PenCapStyle() const return _pencap) ; Qt::PenJoinStyle PenJoinStyle()const return _penjoin; Qt::BrushStyle BrushStyle() const return _brushstyle ; 6 Ez nem az euklideszi hossz, aminek a kiszámítása lassú, hanem az ún. manhattan hossz, ami egyszerűen a koordináták abszolüt értékeinek az összege.

Adatok beállítása. Azokat a mezőket, amelyeknek a beállítása egyszerű (pl. egy soros) magába a header fájlba írjuk be. Ezzel a fájl olvasását nem nehezítjük meg. A bonyolultabb függvények definícióját, pl. a SetPixel()-t azonban a canvas.cpp fájlba fogjuk beírni.. void SetPenWidth(int width) _penwidth = width; void SetPenColor(QColor color) _pencolor = color; void SetPenStyle (Qt::PenStyle &v) _penstyle= v; void SetPenCapStyle(Qt::PenCapStyle &v) _pencap= v; void SetPenJoinStyle(Qt::PenJoinStyle &v) _penjoin= v; void SetBrushStyle(Qt::BrushStyle &v) _brushstyle= v; Rajzolások: void SetPixel(const QPoint &pt, QColor color); void LineTo(const QPoint &endpoint); void DrawEllipse(const QRect &rect); void DrawRect(const QRect & rect); void DrawFillRect(const QRect & rect); void DrawArc(double start, double end); // egy pont kirajzolása A teljes rajzterület törlése: void Clear(); A szabadkézi rajzolást a StartFreeHandDrawing() engedélyezi és a StopFreehandDrawing() tiltja le. Ezek nagyon egyszerűek: void StartFreeHandDrawing() _freehand = true; void StopFreehandDrawing() _freehand = false; A widget méretének megváltoztatását a resizeevent()-ben kezeljük le. Ez fogja meghívni a Canvas Resize() függvényét. A szabadkézi rajzoláshoz le kell kezelni az egér állapotát és mozgását. Mivel ezek az operációs rendszertől származó információk, ezeket eseményekben kell beállítanunk. A QWidget osztályban ezek az események protected-ek, legyenek azok itt is! protected: void mousepressevent(qmouseevent *event) Q_DECL_OVERRIDE; void mousemoveevent(qmouseevent *event) Q_DECL_OVERRIDE; void mousereleaseevent(qmouseevent *event) Q_DECL_OVERRIDE; void paintevent(qpaintevent *event) Q_DECL_OVERRIDE; void resizeevent(qresizeevent *event) Q_DECL_OVERRIDE; A Q_DECL_OVERRIDE azonos a C++ override specifikációjával, aminek hatására a fordító ellenőrzi, hogy az adott függvények valóban egy alap osztálybeli függvényt írnak-e felül. Most tudjuk, hogy ez a helyzet, így el is hagyhattuk volna. Ezzel az osztály deklarációja elkészült. Zárjuk be:

; Ha a header fájl védelmére nem a #pragma once opciót választottuk, akkor a heder file utolsó sora #endif kell legyen. A teljes kód a fájl végén megtalálható Az új widget hozzáadása a szerkesztőhöz Kattintsunk a jobb oldali Stacked Widget-re (neve pnlright)! A Property Editor-ban állítsuk be az első oldalt. Rakjunk rá egy Widget-et és állítsuk be bármelyik elrendezést. Ezzel a widget a teljes jobb oldalt kitölti. A jobb egérgombal előhozott egérmenűben válasszuk ki a Promote to pontot! Egy dialógus ablak jelenik meg: A Promoted class name mezőbe a Canvas, a header file mezőbe a canvas.cpp szöveget írjuk be és nyomjuk meg az Add gombot!:

Nyomjuk meg a Promote gombot, majd a Property Editor-ban írjuk át a widget nevét canvasra! Az új widget programja (canvas.cpp) Adjunk a projektünkhöz egy új cpp fájlt és nevezzük el canvas.cpp-nek! Mindenekelőtt include-oljuk be a canvas.h-t A konstruktor: #include "canvas.h" Canvas::Canvas(QWidget *parent) : QWidget(parent) Egyelőre semmit nem csinálunk benne. A Resize() függvény void Canvas::Resize(QImage *image, QSize newsize)

Mivel csak nagyobb új méret esetén akarjuk a _canvas-t átméretezni, először helyettesítsünk minden kisebb új méretet az eredetivel if (newsize.width() < image->width()) newsize.setwidth(image->width()); if (newsize.height() < image->height()) newsize.setheight(image->height()); Ha nem volt változás nem kell átméretezni if (newsize.width() == image->width() && newsize.height() == image- >height()) return; Volt méret növekedés. Létrehozunk egy ideiglenes QImage-t az új mérettel Kitöröljük a tartalmát: QImage newimage(newsize, QImage::Format_RGB32); newimage.fill(qrgb(255, 255, 255)); Átmásoljuk rá a régi képet a QPainter drawimage() függvényével: QPainter painter(&newimage); painter.drawimage(qpoint(0, 0), *image); Átmásoljuk az új QImage-t a háttértároló QImage-re. Ez annak méreteit is megváltoztatja: És befejeztük. A resizeevent() *image = newimage; void Canvas::resizeEvent(QResizeEvent *event) Kiszámoljuk a héttértároló új néretét Átméretezzük a tárolót int newwidth = qmax(width() + 128, _canvas.width()); int newheight = qmax(height() + 128, _canvas.height()); Resize(&_canvas, QSize(newWidth, newheight)); Meghívjuk az eredeti átméretezési eseményt, csináljon meg mindent, amit még esetleg kell. QWidget::resizeEvent(event);

A paintevent() Egy widget tartalmát kizárólag az ő Paint Event-jében szabad kirajzolni void Canvas::paintEvent(QPaintEvent *event) A QPainter erre a widgetre rajzolja majd a háttértároló tartalmának kijelölt részét 7 : QPainter painter(this); Csak ezt a területet fogja módosítani QRect dirtyrect = event->rect(); Kirajzoljuk a háttértároló tartalmát: A teljes rajzterület törlése: painter.drawimage(dirtyrect, _canvas, dirtyrect); Ez valójában csak a háttértároló fehér színnel való kitöltését jelenti: void Canvas::Clear() _canvas.fill( qrgb(255, 255, 255) ); Elmentjük az utolsó pozícióba a bal felső sarkot _lastpoint = QPoint(0, 0); és beállítjuk, hogy a kép módosult _modified = true; Az update() hívás újra rajzoltatja a teljes widget-et. update(); Egy pixel beállítása: void Canvas::setPixel(const QPoint &pt, QColor color) _canvas.setpixel(pt, color.rgb()); _lastpoint = pt; update(); Alakzatok kirajzolása 7 Szemben az eddigiekkel, amelyekben a háttértárolóra rajzoltunk. Oda bármikor lehetett, a widgetre csak itt.

Először az egyszerűbb eseteket, a rögzített alakzatok kirajzolását tárgyaljuk. Ezekhez a QPainter meglevő függvényeit használjuk, tehát a Canvas osztály függvényeiben csak a paramétereket kell beállítanunk a QPainter számára. A tulajdonképpeni munkát a QPainter végzi el. Egyenes vonal húzása két pont között A vonalhúzást az utolsó használt pontban kezdjük és az endpoint-ban fejezzük be. void Canvas::lineTo(const QPoint &endpoint) Létrehozunk egy QPainter-t a rajzoláshoz QPainter painter(&_canvas); Beállítjuk az aktuális toll paramétereket painter.setpen(qpen(_pencolor, _penwidth, _penstyle, _pencap, _penjoin)); Meghúzzuk a vonalat és beállítjuk, hogy a kép módosult: painter.drawline(_lastpoint, endpoint); _modified = true; Ezután frissítjük a képnek azt a részét ahová a vonalat húztuk. Ehhez az update() nek azt a formáját használjuk, amiben megadjuk a frissítendő téglalapot. Ehhez egy ideiglenes QRect objektumot használunk

A QRect objektum egy egész számokkal megadott téglalapot határoz meg. A téglalapot megadhatjuk bal felső sarkának koordinátáival, szélességével és magasságával: QRect ::QRect(int left, int top, int width, int height); de ezeket egy QPoint és egy QSize objektummal is kifejezhetjük: QRect ::QRect(QPoint lefttop, QSize widthheight ) ; A téglalap adatait természetesen utólag, külön - külön is beállíthatjuk, módosíthatjuk, illetve lekérdezhetjük. A Qt-ban szokásos elnevezési konvenció szerint a beállítások függvénynevei settel kezdődnek és a lekérdezések nevével folytatódna, csak azok ilyenkor nagy betűvel kezdődnek: pl lekérdezés: left(), beállítás setleft(). Egy QRect objektumot sokféle módon manipulálhatunk. Egyik hasznos lehetőség a teljes négyszög bal felső pontját eltolni egy tetszőleges pontba a moveto(int x, int y), vagy moveto(qpoint topleft) függvényekkel, vagy eltolni egy adott dx,dy vektorral a translate(int dx, int dy), vagy translate(qpoint dvector) függvényeket használva. Ezeknek van olyan változatuk is (translated(int dx, int dy), vagy translated(qpoint dvector)), amelyik az eredeti QRect-et nem változtatja meg, hanem létrehoz egy újat az adott transzformáció után. Mivel egy QRect objektum adatait úgy is megadhatjuk, hogy a szélessége és magassága negatív legyen a QRect::normalized() függvény egy olyan másik QRect-et ad vissza, amiben a szélesség és a magasság pozitív. A QRect::adjust(int dx1, int dy1, int dx2, int dy2) függvény a téglalap minden méretét megváltoztatja, míg a QRect::adjusted() egy másik ideiglenes téglalapot ad vissza, amely az eredeti koordináták megváltoztatásával adódik.

A méreteket lekérdezhetjük a QSize size(), vagy a width() és height() függvényekkel. Egyéb lekérdezések is vannak, de a bottom() és right() függvényeket ne használjuk! Azok hibás értékeket adnak vissza! Használjuk helyettük a jobb alsó sarok x és y koordinátájának a meghatározásához a left()+width(), ill. top()+height() formulákat! A contains(const QPoint &pont, bool csakbelül = false) const függvény megadja, hogy az adott pont a téglalapban belül van-e. Ha csakbelül = true, akkor a kerületen levő pontok nem számítanak téglalaphoz. Azt, hogy két téglalapnak van e közös pontja a bool intersects(const QRect másik) függvény adja meg. A két téglalap metszetének megfelelő téglalapot az intersected(const QRect másik), unióját a united(const QRect másik) függvénnyel kaphatjuk meg. Érdemes megjegyezni, hogy ha R, r és s QRect-ek, akkor a következőek ekvivalensek: R = r.intersected(s); R = s.intersected(r); és R = r & s; R = r.united(s); R = s.united(r); és R = r s; A QRectF a QRect valós számokat használó megfelelője. A biztonság kedvéért egy kis rátartással dolgozunk (a képlet magyarázatát a téglalap kirajzolásánál adjuk meg) : int rad = (_penwidth / 2) + 2; update(qrect(_lastpoint, endpoint).normalized().adjusted(-rad, -rad, +rad, +rad)); Végül beállítjuk az utoljára használt pontot _lastpoint = endpoint;

Téglalap A QPainter kétféle téglalapot rajzol ki. Az egyiknek nincs kerete, vagyis, hacsak az ecsetnek nincs színe, nem is látható: Ez a QPainter ::fillrect(). A másik keretet rajzol és, ha az ecsetet külön nem állítjuk be nem tölti ki annak belsejét. Ez a QPainter drawrect(). Most mindkettőt használjuk bár elég lenne a második is az ecset beállításával. A téglalap felrajzolásának függvénye: void Canvas::DrawRect(const QRect & rect, bool fill) Létrehozzuk a festőt QPainter painter(&_canvas); Beállítjuk a tollat painter.setpen(qpen(_param.pencolor, _param.penwidth, _param.penstyle, _param.pencap, _param.penjoin)); És szükség szerint (fill == true) az ecsetet is. A kitöltött, de keret nélküli téglalapra a fillrect() függvényt használjuk if (fill) painter.fillrect(rect, QBrush(_param.brushColor, _param.brushstyle)); A keretet a drawrect.() rajzolja ki painter.drawrect(rect); _modified = true; Kérjük meg a rendszert, hogy az elő adandó alkalommal 8 küldjön kirajzolási üzenetet a widgetnek: int rad = (_param.penwidth / 2) + 2; update(rect.normalized().adjusted(-rad, -rad, +rad, +rad)); A csak drawrect()-et használó, alternatív, téglalap rajzoló programrész így nézne ki: if (fill) painter.setbrush(qbrush(_param.brushcolor, _param.brushstyle)); painter.drawrect(rect); Matematikailag egy téglalapot végtelenül vékony szakaszok határolnak. Kirajzolásnál azonban a toll vastagsága befolyásolja milyen lesz a kirajzolt téglalap: 8 Azaz, amikor nincs már más üzenet az üzenetciklusban.

Ezért a frissítendő téglalap általában nagyobb, mint a matematikai. Balra és fent a toll szélességének felével, jobbra és lent páros vastagságú toll esetén ugyanennyivel, páratlan vastagságú esetén ennél 1 pixellel többel. Az egyszerűség kedvéért azonban mi mindegyik esetben a szükségesnél 2+2 pixellel nagyobb téglalapot veszünk. 1 pixelnél nagyobb vonalvastagság esetén a körvonal egy része lemaradhat a képről, ha a befoglaló téglalap valamely oldala érintkezik az widget szélével. A többi alakzat rajzolása erre a kaptafára történik. Az ellipszisr egy olyan befoglaló QRect téglalapba kerül, amivel az oldalak közepén érintkezik 9. Kirajzolásához a QPainter::drawEllipse(rect) függvényt használjuk. Kör esetében a paraméterek a befoglaló négyzet bal felső sarka és a kör sugara. 9 Ha olyan kirajzolást akarunk megadni, ami az ellipszi középpontját a téglalap középpontjába rakja, a koordináták transzformációjáról nekünk kell gondoskodnunk.

A szabályos háromszög és sokszög 10 esetében nem téglalapot, hanem oldalhosszat, illetve oldal hosszat és oldalszámot kell megadni. Ezeket a QPainter::drawPolygon(QPointF* pontok, int darab) függvénnyel rajzoljuk ki. Tetszőleges zárt sokszög kirajzolása A sokszögek kirajzolására szolgáló függvényünk egy, a pontok koordinátáit tartalmazó, vektort és egy kitöltést jelző flag-et vár.: void Canvas::DrawPolygon(const QVector<QPointF> points, bool fill) Azzal kezdjük, hogy ésszerű pont darabszámot kezelünk csak le 11 : Létrehozzuk a festőt if (points.size() < 3 points.size() > 100) // értelmes korlát a lehetséges pontokra return; QPainter painter(&_canvas); Beállítjuk a tollat painter.setpen(qpen(_param.pencolor, _param.penwidth, _param.penstyle, _param.pencap,_param.penjoin)); És szükség szerint az ecsetet is. if (fill) painter.setbrush(qbrush(_param.brushcolor,_param.brushstyle)) ; Kirajzoljuk a sokszöget. A drawpolygon az utolsó pontot automatikusan összeköti az elsővel. painter.drawpolygon( points.data(), points.size() ); A kép megváltozott _modified = true; Határozzuk meg a befoglaló téglalapot, ami megadja az újrarajzolandó területet! double minx = 10000, maxx = -10000, miny = 10000, maxy = -10000; for (int i = 0; i < points.size(); ++i) if (minx > points[i].x()) minx = points[i].x(); if (maxx < points[i].x()) maxx = points[i].x(); if (miny > points[i].y()) miny = points[i].y(); if (maxy < points[i].y()) maxy = points[i].y(); Módosítsuk a toll vastagság szerint QRect rect = QRect(minx, miny, maxx, maxy); int rad = (_param.penwidth / 2) + 2; Kérjük meg a rendszert, hogy rajzolja újra a képet: 10 A szabályos háromszöget kirajzolhatjuk, mint egy 3 oldalú szabályos sokszöget. 11 Jó programban hiba esetén visszajelzést is kell adjunk a felhasználóknak. Itt ettől eltekintünk.

update(rect.normalized().adjusted(-rad, -rad, +rad, +rad)); A szabadkézi rajz Szabadkézi rajz üzemmódban a rajzolás a bal egér gomb lenyomásával kezdődik és felengedésével végződik. Közben az egérmozgást kell lekövetnünk. Mindezek egy egy esemény lekezelését jelentik: void mousepressevent(qmouseevent *event) Q_DECL_OVERRIDE; void mousemoveevent(qmouseevent *event) Q_DECL_OVERRIDE; void mousereleaseevent(qmouseevent *event) Q_DECL_OVERRIDE; A QMouseEvent esemény (többek között) az alábbi információkat tartalmazza: 1. Qt::MouseButton button() const Melyik gomb keltette ezt az eseményt. Ha az esemény az egér mozgása, akkor ez mindig Qt::NoButton. Maximum 27 nem görgető gombhoz 12 tartozó szám jelenhet meg itt. Leggyakrabban a Qt::LeftButton, Qt::RightButton és Qt::MiddleButton értékeket használjuk. 2. Qt::MouseButtons buttons() const (figyeljünk az s-re a név végén!) Milyen gombok voltak lenyomva, amikor az esemény létrejött. A nyomva tartott nem görgető gombokhoz tartozó számok OR-ral összekapcsolva. 3. QPoint pos() const, int x() const, int y() const Az egér koordinátái annak a widget-nek a bal felső sarkához nézve, ami felett megnyomtuk az egér gombját. 4. QPointF screenpos() Elvileg az egér koordinátái a képernyőhöz képest 5. Qt::KeyboardModifiers modifiers() Ez minden beviteli esemény esetén létezik: azok a speciális billentyűk, amelyek közvetlenül az (egér) esemény előtt lenyomva voltak (Ctrl, Alt, Windows gomb) 12 A görgetés az egéren levő gomb(okk)al egy speciális QWheelEvent esemény, nem gombként kell lekezelni. Mobil eszközökön a QTouchEvent eseményeket is le kell kezeljük.

Amikor az egér bal gombját megnyomjuk akkor ellenőrizzük, hogy szabadkézi rajz módban vagyunk e, és, ha igen, akkor a _drawing jelzővel jelezzük, hogy mostantól szabad rajzolnunk a mousemove() eseményben. void Canvas::mousePressEvent(QMouseEvent *event) if (event->button() == Qt::LeftButton) if(_freehand) _drawing = true; A tulajdonképpeni rajzolást a mousemove() eseményben végezzük el, amennyiben rajzolás közben vagyunk, azaz _drawing == true: void Canvas::mouseMoveEvent(QMouseEvent *event) if (_drawing) LineTo(event->pos()); A bal egérgomb felengedésével a rajzolást befejezzük. Ezt a _drawing false-ra állításával tesszük meg. Mivel rajzolás közben benyomhattunk másik egérgombot is a felengedésnél ellenőriznünk kell melyiket engedtük fel. Meghúzzuk az utolsó vonaldarabot 13, majd az egér koordinátáját beállítjuk az utolsó használt pont koordinátájának: void Canvas::mouseReleaseEvent(QMouseEvent *event) if (_drawing & event->button() == Qt::LeftButton) LineTo(event->pos()); _lastpoint = event->pos(); _drawing = false; Fordítsuk le a programot és próbáljuk ki a szabadkézi rajzolást! Azt fogjuk tapasztalni, hogy függetlenül a toll beállításaitól a rajzunk mindig nagyon hasonló lesz és nem tükrözi a toll stílusának beállított értékeket. Mi ennek az oka? Hogyan lehetne kijavítani? Egyszerű e a javítás? 13 Ha ezt nem tennénk meg, akkor egy pontot nem tudnánk kirajzolni az egérrel.