[c++ / SFML] [µPaint][2] Ein minimalistisches Zeichenprogramm

in #de-stem7 years ago (edited)

Thumbnail3.jpg
English version: here
Demonstrations-video: hier
Wegen der positiven Reaktionen auf den ersten Teil hier nun der Followup, in dem das Programm um die Möglichkeit zum Zeichnen von Rechtecken erweitert wurde.
Wie im ersten Teil, soll auch dieser zeigen, wie einfach es sein kann ein eigenes kleines Zeichenprogramm zu programmieren bzw. dieses zu erweitern. Implementierbare Features wären z.b. noch (nach Aufwand sortiert): Kreise, zoomen oder das Löschen des zuletzt hinzugefügten Elements durch Strg + Z .

Weiterentwicklungen des Codes, Fragen, Anregungen, etc. gerne in den Kommentare hinterlassen.

Setup

Es wurde c++ und die SimpleFastMultimediaLibrary (SFML) zum erstellen dieses Programms benutzt. Installation von SFML für VisualStudio: hier

Da steemit (noch) kein Syntax-Highlighting unterstützt, kann der unten gezeigte code zur besseren Lesbarkeit in einen Editor, wie Notepad++ kopiert werden.

Code

Wie im ersten Teil wurden auch hier wieder VertexArrays verwendet.
Wieso?
Wesentlich geringerer Speicherbedarf. Alternativ zu VertexArrays bietet SFML auch RectangleShapes. Beim betrachten der Member_Variablen in Shape.hpp (von der RectangleShape erbt) sollte klar werden, dass das RectangleShape mehr Speicherplatz belegt.
Um aber genau zu sein: RectangleShapevsVertexArray.jpg (Siehe code)
Man Beachte bei der Speicherbedarfmessung, dass die Größe des RectangleShapes minimal und die Größe des VertexArrays (so wie es in meinem Code verwendet wird) maximal war.

Funktionsweise (Benutzer)

Mehrere Ansätze sind möglich. Es würde z.b. auch reichen, wenn man nur vollständig ausgefüllte Rechtecke zeichnen wollen würde, dass nur 4 vertices für jeden Eckpunkt verwendet werden (siehe sf::Quads). Mit dieser Methode würden zwei Benutzereingaben, nämlich zwei gegenüberliegende Eckpunkte zum konstruieren des Rechtecks, reichen.

Um einen beliebig großen Rand zu realisieren werden 3 Punkte benötigt:
Maus Button loslassen -> Position Punkt 1
Maus Button drücken -> Position Punkt 2
Maus Button loslassen -> Position Punkt 3 (Final)

Punkt 2 ist nur dann != Punkt 3, wenn der Maus Button gedrückt gehalten wird und die Maus vor dem Loslassen verschoben wird.
Für den Benutzer heißt das: Er klickt einmal für Position 1, dann verschiebt er seine Maus, je nach gewünschter Randgröße, klickt und hält gedrückt, verschiebt seine Maus an die Endposition und lässt los.

Hinweis: Um zwischen den Zeichenmodi Rechteck und Linie hin und her zu wechseln, müssen die Tasten "r" bzw "l" gedrückt werden.

Funktionsweise (Programmablauf)

Die Variable "R_first_MB_released" gibt an, ob der Benutzer den ersten Punkt schon festgelegt hat, oder nicht.
"R_locked" wiederum, gibt die Berechnung des Rechtecks, wenn zwei Punkte bereits definiert wurden frei. (Es wird angenommen, dass der 3. Punkt auf der aktuellen Mausposition liegt -> Preview des Rechtecks beim Zeichnen möglich)

Berechnung der 16 vertices

Rechtecksberechnung.jpg

4 Punkte bilden ein Trapez (sf::Quads) und werden wie oben zu sehen nur durch ihre Positionen gelinkt.
Punkt 0 und Punkt 15 sind definiert durch den 1. vom Benutzer festgelegten Punkt.
Punkt 1 und 14 durch die 2.
Punkt 8 und 7 werden auf die derzeitige Maus Position gesetzt (Preview möglich).
Beachte: Maus Position - Koordinaten mapping, wie in Teil 1 beschrieben.

Alle anderen Punkte können daraus berechnet werden. Dazu betrachtet man jeweils den x und y Teil eines Punktes separat.
Z.B. Punkt 6 (7.x - (1.x - 0.x ) | 7.y - (1.y - 0.y))

Nebeneffekt des Ansatzes: Je nachdem wo man die 3 Punkte definiert, kann man neben Rechtecken auch 6 Ecke und um 45° gedrehte gefüllte Rechtecke zeichnen: 6Ecke.jpg

c++


#include "SFML\Graphics.hpp"
#include <time.h> // Nur für srand()
#include <iostream>
#include <vector>


int main()
{

    srand(time(NULL)); // rand() mit der aktuellen Zeit initialisieren

    std::vector<sf::VertexArray> vertices; // vector in dem alle vertexArrays (Linien, Rechtecke) gespeichert werden

    enum class states { Line, Rectangle, Circle }; // Derzeitiger Zeichenmodus
    states current_state = states::Line;

    // Linien
    vertices.push_back(sf::VertexArray()); // Die 1. Linie
    vertices[0].setPrimitiveType(sf::LinesStrip); // Der PrimitiveType der 1. Linie: see https://www.sfml-dev.org/tutorials/2.4/graphics-vertex-array.php
    // int lines_number = 0; // Index der aktuellen Linie -> fällt raus, weil nur das letzte Element erreicht werden muss und das mit vertices[vertices.size() -1] errreicht werden kann
    bool L_locked = false; // (Nur im Linien Modus) Wenn ein Maus Button gedrückt wurde wird diese Variable true, wenn einer losgelassen wird false


    // Rechtecke
    bool R_first_MB_released = false; // (Nur im Rechtecks-Modus) Wird true, wenn zum ersten mal ein Mouse Button losgelassen wurde, false beim zurücksetzen
    sf::Vector2f R_first; // Position des ersten losgelassenen Mous Buttons
    sf::Vector2f R_second; // Zweite Position (Mouse Button gedrückt, nachdem R_first gesetzt wurde); Dritte Position = aktuelle Mouse_pos
    bool R_locked = false; 
    sf::VertexArray construction(sf::Quads); // Das Rechteck, das aktuell konstruiert wird
    for (int i = 0; i < 16; i++) // Vorbelegen
        construction.append(sf::Vertex());

    //sf::RectangleShape s;
    //std::cout << "RectangleShape: " << sizeof(s) << ", VertexArray: " << sizeof(construction) << std::endl; std::cin.get(); std::cin.get(); // RectangleShape vs VertexArray
    

    sf::Color curr_col = sf::Color::Black; // Farbe der vertices
    sf::Vector2i last_Mouse_pos(0, 0);

    sf::RenderWindow window(sf::VideoMode(1280, 720), "µPaint", sf::Style::Close, sf::ContextSettings(0, 0, 0)); // Das Fenster in dem alles gezeichnet wird
    window.setFramerateLimit(60);

    sf::Vector2i Border_Offset(-5, -25); // Siehe Eigenheiten (Hintergrundwissen in Teil 1: https://steemit.com/de-stem/@numbo/c-sfml-paint-ein-minimalistisches-zeichenprogramm ): Rand

    while (window.isOpen())
    {

        // Events
        sf::Event event;
        while (window.pollEvent(event))
        {

            if (event.type == sf::Event::KeyPressed) // Fenster schließen
                if (event.key.code == sf::Keyboard::Key::Escape)
                    window.close();
            if (event.type == sf::Event::Closed) // Fenster schließen
                window.close();

            if (event.type == sf::Event::KeyPressed) // Zwischen den Zeichenzuständen wechseln
            {
                if (event.key.code == sf::Keyboard::Key::L)
                    current_state = states::Line;
                else if (event.key.code == sf::Keyboard::Key::R)
                    current_state = states::Rectangle;
                else if (event.key.code == sf::Keyboard::Key::C)
                    current_state = states::Circle;
            }

            if (event.type == sf::Event::MouseButtonPressed)
            {
                if (current_state == states::Line)
                    L_locked = true;
                else if (current_state == states::Rectangle)
                {
                    if (R_first_MB_released) // Nur wenn erster Punkt des Rechtecks schon definiert ist den zweiten Punkt festlegen
                    {
                        R_second = sf::Vector2f(sf::Mouse::getPosition() - window.getPosition() + Border_Offset);
                        R_locked = true;
                    }
                }
                else if (current_state == states::Circle)
                {
                    // Selber implementieren 
                }

            }

            if (event.type == sf::Event::MouseButtonReleased) // Siehe "locked" Definition
            {

                if (current_state == states::Line)
                {
                    // Neue Linie hinzufügen
                    vertices.push_back(sf::VertexArray());
                    vertices[vertices.size() - 1].setPrimitiveType(sf::LinesStrip);

                    L_locked = false; // Reset Linie
                }
                else if (current_state == states::Rectangle)
                {
                    if (!R_first_MB_released) // Erste Position festgelegt
                    {
                        R_first_MB_released = true;
                        R_first = sf::Vector2f(sf::Mouse::getPosition() - window.getPosition() + Border_Offset);
                    }
                    if (R_first_MB_released && R_locked) // Finale Positon festegelegt -> speichern und zurücksetzen
                    {
                        vertices.push_back(construction);

                        R_first_MB_released = false;
                        R_locked = false;

                        curr_col = sf::Color::Color(rand() % 255, rand() % 255, rand() % 255);

                        for (int i = 0; i < construction.getVertexCount(); i++) // Würde sonst noch mitgezeichnet werden, obwohl das eigentliche Rechteck schon in "vertices" gespeichert wurde -> Zeichnen über construction würde nicht funktionieren. 
                            construction[i].position = sf::Vector2f(0, 0);
                    }
                }
                else if (current_state == states::Circle)
                {
                    // Selber implementieren
                }



            }
        } // Ende Events


        //Linie konstruieren
        if (L_locked) // Siehe "locked" Definition
        {
            if (last_Mouse_pos != sf::Mouse::getPosition()) // Nur hinzufügen, wenn sich die Maus bewegt hat (Arbeitsspeicher nicht sinnlos verschwenden)
            {
                //.append(Position, Farbe) : .append(MousePos - WindowPos + MouseOffset, curr_col)
                vertices[vertices.size() - 1].append(sf::Vertex(sf::Vector2f(sf::Mouse::getPosition().x - window.getPosition().x + Border_Offset.x, sf::Mouse::getPosition().y - window.getPosition().y + Border_Offset.y), curr_col));

                last_Mouse_pos = sf::Mouse::getPosition();
            }
        }

        // Rechteck konstruieren
        if (R_locked)
        {
            if (last_Mouse_pos != sf::Mouse::getPosition())
            {
                //Hier aktuelles Rechteck berechnen (siehe Skizze in Teil 2)
                // = QuadsStrip DIY

                sf::Vector2f render_mouse_pos(sf::Mouse::getPosition() - window.getPosition() + Border_Offset); // siehe Maus-Koordinaten mapping in Teil 1
                
                construction[0] = sf::Vertex(R_first, curr_col);
                construction[1] = sf::Vertex(R_second, curr_col);
                construction[2] = sf::Vertex(sf::Vector2f(render_mouse_pos.x - (R_second.x - R_first.x), R_second.y), curr_col);
                construction[3] = sf::Vertex(sf::Vector2f(render_mouse_pos.x, R_first.y), curr_col);
                            
                construction[4] = sf::Vertex(construction[3].position, curr_col);
                construction[5] = sf::Vertex(construction[2].position, curr_col);
                construction[6] = sf::Vertex(sf::Vector2f(render_mouse_pos.x - (R_second.x - R_first.x), render_mouse_pos.y - (R_second.y - R_first.y)), curr_col);
                construction[7] = sf::Vertex(sf::Vector2f(render_mouse_pos), curr_col);
                            
                construction[8] = sf::Vertex(construction[7].position, curr_col);
                construction[9] = sf::Vertex(construction[6].position, curr_col);
                construction[10] = sf::Vertex(sf::Vector2f(construction[1].position.x, construction[6].position.y), curr_col);
                construction[11] = sf::Vertex(sf::Vector2f(construction[0].position.x, construction[7].position.y), curr_col);
                            
                construction[12] = sf::Vertex(construction[11].position, curr_col);
                construction[13] = sf::Vertex(construction[10].position, curr_col);
                construction[14] = sf::Vertex(construction[1].position, curr_col);
                construction[15] = sf::Vertex(construction[0].position, curr_col);

                last_Mouse_pos = sf::Mouse::getPosition(); // Maus position zurücksetzen

            }
        }

        //curr_col = sf::Color::Color(rand() % 255, rand() % 255, rand() % 255);
        

        //std::cout << "vertices in line " << lines_number << ": " << vertices[lines_number].getVertexCount() << std::endl;

        std::cout << "V1: " << R_first.x << " | " << R_first.y << " V2: " << R_second.x << " | " << "vertices-vec: " << vertices.size() << std::endl;

        window.clear(sf::Color::White); // Die Leinwand mit einener bestimmten Farbe löschen

        // Alles zeichnen
        for (int i = 0; i < vertices.size(); i++) // Die ersten Objekte zuerst zeichnen -> Am weitesten hinten (Überschneidungen)
        {
            window.draw(vertices[i]);
        }

        window.draw(construction); // Konstruktions-Rechteck zeichnen -> für preview

        window.display();
    }


    return 0;
}

Sort:  

Hallo @numbo, herzlich willkommen auf Steemit.

Wenn Du Fragen zu Steemit hast, oder Dich mit anderen deutschen „Steemians“ austauschen magst, schau einfach mal auf unserem Discord-Server https://discord.gg/g6ktN45 vorbei.

Unter dem folgenden Link findest Du einige Anleitungen, die Dir den Einstieg in das Steem-Universum deutlich erleichtern werden: Deutschsprachige Tutorials für Steemit-Neulinge: Ein Überblick