Gtkmm : 用开罗绘图

时间:2021-07-31 13:52:35

标签: c++ gtk draw cairo gtkmm

使用 Gtkmm 和 Cairo,我希望能够在照片上绘制不同的形状。在窗口的标题栏中,我有两个按钮代表要绘制的形状(圆形和矩形)。当您单击其中之一时,您可以绘制其关联的形状。这是 mt 代码:

MyWindow.cpp

#include "MyWindow.h"

MyWindow::MyWindow()
        : circleButton("circle"),
          rectangleButton("rectangle ") {

    set_default_size(700, 700);
    set_position(Gtk::WIN_POS_CENTER);

    header.set_show_close_button(true);
    header.pack_start(rectangleButton);
    header.pack_start(circleButton);;

    set_titlebar(header);

    // Dwg is an instance of Drawing class
  
  circleButton.signal_clicked().connect([&] {
        Dwg.switch_to_circle();
    });
    rectangleButton.signal_clicked().connect([&] {
        Dwg.switch_to_rectangle();
    });

    add(Dwg);
    show_all();
}

绘图.h

#ifndef DRAWING_H
#define DRAWING_H

#include <gtkmm.h>
#include <cairo/cairo.h>

class MyDrawing : public Gtk::Layout {
public:
    MyDrawing();

    ~MyDrawing();

    void switch_to_circle();
    void switch_to_rectangle();

protected:
    virtual bool draw_image(const Cairo::RefPtr<::Cairo::Context> &cr);
    virtual bool draw_rectangle(const Cairo::RefPtr<::Cairo::Context> &cr);
    virtual bool draw_circle(const Cairo::RefPtr<::Cairo::Context> &cr);


private:

    Glib::RefPtr<Gdk::Pixbuf> pix;

    double beginPoint_x, beginPoint_y, endPoint_x, endPoint_y, lineWidth,width,height;

    bool isDrawRectangle;
};

#endif // DRAWING_H

绘图.cpp

#include <iostream>
#include "MyDrawing.h"
#include <cairomm/context.h>
#include <cairomm/surface.h>


MyDrawing::MyDrawing()
        : isDrawRectangle(true),
          width(20),
          height(20) {

    pix = Gdk::Pixbuf::create_from_file("file.svg", 500, 500);
    if (pix) {
        this->signal_draw().connect(sigc::mem_fun(*this, &MyDrawing::draw_image));
    }
    add_events(Gdk::BUTTON1_MOTION_MASK | Gdk::BUTTON_PRESS_MASK);

    signal_button_press_event().connect([&](GdkEventButton *e) {
        this->beginPoint_x = e->x;
        this->beginPoint_y = e->y;
        if(isDrawRectangle) {
            this->signal_draw().connect(sigc::mem_fun(*this, &MyDrawing::draw_rectangle));
            queue_draw();
        }
        else {
            this->signal_draw().connect(sigc::mem_fun(*this, &MyDrawing::draw_circle));
            queue_draw();
        }
        return true;
    });

    signal_motion_notify_event().connect([&](GdkEventMotion *e) {
        this->endPoint_x = e->x;
        this->endPoint_y = e->y;
        width = endPoint_x - beginPoint_x;
        height = endPoint_y - beginPoint_y;

        if(isDrawRectangle) {
            this->signal_draw().connect(sigc::mem_fun(*this, &MyDrawing::draw_rectangle));
            queue_draw();
        }
        else {
            this->signal_draw().connect(sigc::mem_fun(*this, &MyDrawing::draw_circle));
            queue_draw();
        }

        return true;
    });
}

MyDrawing::~MyDrawing() = default;

bool MyDrawing::draw_image(const Cairo::RefPtr<::Cairo::Context> &cr) {
    std::cout << "signal img" << std::endl;
    if (pix) {
        cr->save();
        Gdk::Cairo::set_source_pixbuf(cr, pix, 100, 100);
        cr->rectangle(0, 0, get_width(), get_height());
        cr->fill();
        cr->restore();
    }
    return false;
}

bool MyDrawing::draw_rectangle(const Cairo::RefPtr<::Cairo::Context> &cr) {
    std::cout << "signal square" << std::endl;
    cr->save();
    cr->set_line_width(10);
    cr->set_source_rgba(0., 0., 1., 1.);
    cr->rectangle(beginPoint_x, beginPoint_y, width, height);
    cr->stroke();
    cr->save();
    cr->restore();

    return false;
}

bool MyDrawing::draw_circle(const Cairo::RefPtr<::Cairo::Context> &cr) {
    std::cout << "signal square" << std::endl;

    cr->save();
    cr->set_line_width(10);
    cr->set_source_rgba(0., 0., 1., 1.);
    cr->arc(beginPoint_x, beginPoint_y, width, 0, 2 * M_PI);
    cr->stroke();
    cr->restore();

    return false;
}

void MyDrawing::switch_to_circle() {
    isDrawRectangle = false;

}

void MyDrawing::switch_to_rectangle() {
    isDrawRectangle = true;
}

当我单击另一个形状时,以前的形状一直显示在绘图区域上,而新的形状会在其上绘制。另一方面,当信号断开时,相应的形状也会从屏幕上消失。如何确保形状一直显示?

2 个答案:

答案 0 :(得分:0)

我不确定是什么让您从 Gtk::Layout 继承而不是使用标准的 Gtk::DrawingArea,但我使用与您类似的设计创建了一个简化(且有效)的示例。

基本思想是,当用户完成绘制形状(停止拖动并释放鼠标按钮)时,会发生以下情况:

  1. 窗口的当前状态(就其上绘制的内容而言)保存到 Gtk::Pixbuf
  2. 那个 Gtk::PixBuf 画在窗户上。

这意味着在 1. 中,最后绘制的形状也保存在缓冲区中。当 2. 发生时,在窗口上重新绘制,因此不会消失。这是代码,您需要对其进行一些调整以适应您的情况。首先,一个绘图助手:

class DrawHelper : public Gtk::Layout
{

public:

    DrawHelper();
    ~DrawHelper();

private:

    bool draw_image(const Cairo::RefPtr<::Cairo::Context>& p_context);
    bool draw_rectangle(const Cairo::RefPtr<::Cairo::Context>& p_context);
    bool add_current_shape(const Cairo::RefPtr<::Cairo::Context>& p_context);

    Glib::RefPtr<Gdk::Pixbuf> m_buffer;

    double m_startX;
    double m_startY;

    double m_endX;
    double m_endY;

    double m_width;
    double m_height;

    sigc::connection m_drawConnection;
};

负责进行实际绘图和处理连接。它是这样实现的:

DrawHelper::DrawHelper()
{
    // Create a pixel buffer containing the background image:
    m_buffer = Gdk::Pixbuf::create_from_file("file.svg", DEFAULT_WIDTH, DEFAULT_HEIGHT);
    signal_draw().connect(sigc::mem_fun(*this, &DrawHelper::draw_image));

    // Enable signals:
    add_events(Gdk::BUTTON1_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK);

    // Save initial pointer position when clicked:
    signal_button_press_event().connect(
        [this](GdkEventButton* p_event)
        {
            m_startX = p_event->x;
            m_startY = p_event->y;

            return true;
        });

    // Update rectangle when mouse is dragged:
    signal_motion_notify_event().connect(
        [this](GdkEventMotion* p_event)
        {
            m_endX = p_event->x;
            m_endY = p_event->y;

            m_width  = m_endX - m_startX;
            m_height = m_endY - m_startY;

            signal_draw().connect(sigc::mem_fun(*this, &DrawHelper::draw_rectangle));
            queue_draw();

            return true;
        });

    // Change background so it includes the shape just drawn by
    // the user:
    signal_button_release_event().connect(
        [this](GdkEventButton* p_event)
        {
            // Notice we save to connection to later disconnect it:
            m_drawConnection = signal_draw().connect(sigc::mem_fun(*this, &DrawHelper::add_current_shape));

            return true;
        });
}

DrawHelper::~DrawHelper() = default;

bool DrawHelper::draw_image(const Cairo::RefPtr<::Cairo::Context>& p_context)
{
    Gdk::Cairo::set_source_pixbuf(p_context, m_buffer, 0, 0);

    p_context->paint();

    return false;
}

bool DrawHelper::draw_rectangle(const Cairo::RefPtr<::Cairo::Context>& p_context)
{
    p_context->save();

    p_context->set_line_width(2);
    p_context->rectangle(m_startX, m_startY, m_width, m_height);
    p_context->stroke();

    p_context->restore();

    return false;
}

bool DrawHelper::add_current_shape(const Cairo::RefPtr<::Cairo::Context>& p_context)
{
    // Save the current drawing, including the last drawn
    // shape. This will become the new background (which will
    // visually preserve the last drawn shape).
    m_buffer = Gdk::Pixbuf::create(p_context->get_target(), 0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT);
    Gdk::Cairo::set_source_pixbuf(p_context, m_buffer, 0, 0);

    p_context->paint();

    // We disconnect the signal because we do not want it
    // to keep getting called:
    m_drawConnection.disconnect();

    return false;
}

然后,一个窗口来保存这个助手并将其显示给用户:

class MyWindow : public Gtk::Window
{

public:

    MyWindow();

private:

    DrawHelper m_drawHelper;

};

MyWindow::MyWindow()
{
    set_default_size(DEFAULT_WIDTH, DEFAULT_HEIGHT);

    // Add draw helper:
    add(m_drawHelper);

    // Show all widgets:
    show_all();
}

然后,主要运行它:

#include <gtkmm.h>
#include <cairo/cairo.h>
#include <cairomm/context.h>
#include <cairomm/surface.h>

constexpr int DEFAULT_WIDTH = 500;
constexpr int DEFAULT_HEIGHT = 500;

// DrawHelper here ...
// MyWindow here ...


int main(int argc, char *argv[])
{
    auto app = Gtk::Application::create(argc, argv, "org.gtkmm.examples.base");
  
    MyWindow window;
  
    return app->run(window);
}

话虽如此,我建议您改用经典的 Gtk::DrawingArea 并重载 on_draw 信号处理程序。这将使所有这些更容易理解,并且在线文档对您更有帮助。

答案 1 :(得分:0)

如果您仍然感兴趣,我为您提供了另一种解决方案。您可以直接保存它们的参数并重新绘制它们,而不是在背景图像上保存已经绘制的形状。我编写了一个示例程序来执行此操作:

#include <memory>
#include <vector>

#include <gtkmm.h>
#include <cairo/cairo.h>
#include <cairomm/context.h>
#include <cairomm/surface.h>

constexpr int DEFAULT_WIDTH = 500;
constexpr int DEFAULT_HEIGHT = 500;
constexpr double LINE_WIDTH = 2.0;

// Free functions for drawing shapes:
namespace
{

void DrawRectangle(const Cairo::RefPtr<Cairo::Context>& p_context,
                   double p_startX,
                   double p_startY,
                   double p_width,
                   double p_height)
{
    p_context->save();

    p_context->set_line_width(LINE_WIDTH);
    p_context->set_source_rgba(0, 0, 1, 1);
    p_context->rectangle(p_startX, p_startY, p_width, p_height);
    p_context->stroke();

    p_context->restore();
}

void DrawCircle(const Cairo::RefPtr<Cairo::Context>& p_context,
                double p_startX,
                double p_startY,
                double p_width)
{
    p_context->save();

    p_context->set_line_width(LINE_WIDTH);
    p_context->set_source_rgba(0, 0, 1, 1);
    p_context->arc(p_startX, p_startY, p_width, 0, 2 * M_PI);
    p_context->stroke();

    p_context->restore();
}

}

// Shape interface:
//
//  A shape represents a 2D geometric shape a user can draw on the
//  Drawing area. All shapes implement a 'Draw' method which is where
//  the drawing logic resides.
class IShape
{

public:

    virtual ~IShape() = default;

    virtual void Draw(const Cairo::RefPtr<Cairo::Context>& p_context) = 0;
};

// Rectangle shape:
class Rectangle : public IShape
{

public:

    Rectangle(double p_left, double p_up, double p_width, double p_height)
    : m_left{p_left}
    , m_up{p_up}
    , m_width{p_width}
    , m_height{p_height}
    {}

    void Draw(const Cairo::RefPtr<Cairo::Context>& p_context) override
    {
        DrawRectangle(p_context, m_left, m_up, m_width, m_height);
    }

private:
    
    double m_up;
    double m_left;
    double m_width;
    double m_height;

};

// Circle shape:
class Circle : public IShape
{

public:    

    Circle(double p_cX, double p_cY, double p_radius)
    : m_cX{p_cX}
    , m_cY{p_cY}
    , m_radius{p_radius}
    {}

    void Draw(const Cairo::RefPtr<Cairo::Context>& p_context) override
    {
        DrawCircle(p_context, m_cX, m_cY, m_radius);
    }

private:

    double m_cX;
    double m_cY;
    double m_radius;
};

// Draw helper:
//
//  This class represents the widget onto which the user can drawn. Under
//  the hood, this is a Gtk::Drawing area with some signal handlers defined
//  to draw shapes on user action.
//
//  All drawing occurs in the 'on_draw' method, and all signal handlers to
//  is to handle the data (e.g positions, dimentsions, etc) for the 'on_draw'
//  method to work appropriately.
//
//  The 'SetCurrentShape' method can be used to tell the helper which shape
//  to draw.
class DrawHelper : public Gtk::DrawingArea
{

public:

    enum class Shape
    {
        None,
        Rectangle,
        Circle,
    };

    DrawHelper()
    {
        add_events(Gdk::BUTTON1_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK);

        // Click, drag and release signal handlers:
        signal_button_press_event().connect(  [this](GdkEventButton* p_event){return OnButtonPressed(p_event);} );
        signal_motion_notify_event().connect( [this](GdkEventMotion* p_event){return OnMouseMotion(p_event);}   );
        signal_button_release_event().connect([this](GdkEventButton* p_event){return OnButtonReleased(p_event);});
    }

    void SetCurrentShape(Shape p_shape)
    {
        m_currentShape = p_shape;
    }

private:

    // All drawing occurs here and only here:
    bool on_draw(const Cairo::RefPtr<Cairo::Context>& p_context) override 
    {
        // Draw background:
        if(!m_buffer)
        {
            m_buffer = Gdk::Pixbuf::create_from_file("file.svg", DEFAULT_WIDTH, DEFAULT_HEIGHT);
        }

        Gdk::Cairo::set_source_pixbuf(p_context, m_buffer, 0, 0);
        p_context->paint();

        // Draw previously drawn shapes:
        for(const auto& shape : m_alreadyDrawn)
        {
            shape->Draw(p_context);
        }

        // Draw current shape:
        if(m_currentShape == Shape::Rectangle)
        {
            DrawRectangle(p_context, m_startX, m_startY, m_width, m_height);
        }

        if(m_currentShape == Shape::Circle)
        {
            DrawCircle(p_context, m_startX, m_startY, m_width);
        }


        return false;
    }

    bool OnButtonPressed(GdkEventButton* p_event)
    {
        m_startX = p_event->x;
        m_startY = p_event->y;

        return true;
    }

    bool OnMouseMotion(GdkEventMotion* p_event)
    {
        m_endX = p_event->x;
        m_endY = p_event->y;

        m_width  = m_endX - m_startX;
        m_height = m_endY - m_startY;

        queue_draw();

        return true;
    }

    bool OnButtonReleased(GdkEventButton* p_event)
    {
        if(m_currentShape == Shape::Rectangle)
        {
            m_alreadyDrawn.push_back(std::make_unique<Rectangle>(m_startX, m_startY, m_width, m_height));
        }

        if(m_currentShape == Shape::Circle)
        {
            m_alreadyDrawn.push_back(std::make_unique<Circle>(m_startX, m_startY, m_width));
        }

        return true;
    }

    Shape m_currentShape = Shape::None;
    Glib::RefPtr<Gdk::Pixbuf> m_buffer;

    double m_startX;
    double m_startY;

    double m_endX;
    double m_endY;

    double m_width;
    double m_height;

    std::vector<std::unique_ptr<IShape>> m_alreadyDrawn;

};

// Main window:
//
//  This window holds all widgets. Through it, the user can pick a shape
//  to draw and use the mouse to draw it.
class MyWindow : public Gtk::Window
{

public:

    MyWindow()
    : m_drawRectangleBtn{"Rectangle"}
    , m_drawCircleBtn{"Circle"}
    {
        set_default_size(DEFAULT_WIDTH, DEFAULT_HEIGHT);

        m_headerBar.set_show_close_button(true);
        m_headerBar.pack_start(m_drawRectangleBtn);
        m_headerBar.pack_start(m_drawCircleBtn);;
        set_titlebar(m_headerBar);
        add(m_drawArea);

        m_drawRectangleBtn.signal_clicked().connect([this](){OnRectangleBtnClicked();});
        m_drawCircleBtn.signal_clicked().connect([this](){OnCircleBtnClicked();});

        show_all();
    }

private:

    Gtk::HeaderBar m_headerBar;
    Gtk::Button    m_drawRectangleBtn;
    Gtk::Button    m_drawCircleBtn;
    DrawHelper     m_drawArea;

    void OnRectangleBtnClicked()
    {
        m_drawArea.SetCurrentShape(DrawHelper::Shape::Rectangle);
    }

    void OnCircleBtnClicked()
    {
        m_drawArea.SetCurrentShape(DrawHelper::Shape::Circle);
    }

};

int main(int argc, char *argv[])
{
    auto app = Gtk::Application::create(argc, argv, "org.gtkmm.examples.base");
  
    MyWindow window;
  
    return app->run(window);
}

每次用户释放鼠标按钮时,绘制的形状(与释放时的参数一起)保存到 std::vector 中作为 IShape,它有一个 {{1} }, 方法。稍后可以调用此方法来重绘形状。然后,在 Draw 处理程序中,重新绘制所有先前绘制的形状,将它们留在屏幕上。请注意,我在这里使用了 on_draw,这比您的方法更典型。我想向您展示在我看来可以使代码更简洁的替代方案(不会弄乱处理程序回调)。

最后一点,可能的增强是可能的(还有更多,这些只是我在写这篇文章时想到的一些):

  • 您可以通过缓存一些内容而不是每次都重新绘制所有内容来降低性能成本。
  • 您可以通过在对 Gtk::DrawingArea 的调用中使用参数来降低性能成本,这样就不会不断重绘整个小部件(仅重绘更改的部分)。
  • 您可以使用工厂来创建形状。这会将形状创建与仅知道 queue_draw 接口的其余代码分离。如果您想添加形状,它还将使您的程序更易于维护。
相关问题