Gtkmm:用 cairo 绘图

Gtkmm : Drawing with cairo

使用 Gtkmm 和 Cairo,我希望能够在照片上绘制不同的形状。在我的 window 的 header 栏中,我有两个按钮代表要绘制的形状(圆形和矩形)。当您单击其中一个时,您可以绘制其关联的形状。这是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();
}

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

Drawing.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;
}

当我点击另一个形状时,上一个形状一直显示在绘图区,新的形状被绘制在上面。另一方面,当信号断开时,相应的形状也会从屏幕上消失。我怎样才能确保形状一直显示?

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

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

  1. window 的当前状态(根据其上绘制的内容)保存到 Gtk::Pixbuf
  2. 那个Gtk::PixBuf是画在window上的。

这意味着在1.中,最后绘制的形状也保存在缓冲区中。当 2. 发生时,在 window 上重新绘制,因此不会消失。这是代码,您需要根据您的情况稍微调整一下。首先是绘图助手:

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;
}

然后,window 保存这个助手并将其显示给用户:

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 信号处理程序。这将使所有这些更容易理解,在线文档将对您更有帮助。

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

#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,其中有一个 Draw,方法。稍后可以调用此方法来重绘形状。然后,在 on_draw 处理程序中,重新绘制所有先前绘制的形状,将它们留在屏幕上。请注意,我在这里使用了 Gtk::DrawingArea ,这比您的方法更典型。我想向您展示另一种选择,在我看来,它可以使代码更清晰(不会弄乱处理程序回调)。

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

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