画直线

现在我们已经了解了开罗图形库的基本知识,是时候开始画图了。我们将从画最简单的元素:直线 开始。但是在此之前你需要对开罗的坐标系有所了解。开罗的坐标系原点在窗口的左上角,从左向右X的值逐渐增大,从上往下Y的值逐渐增大。

由于开罗图形库是为了支持多个输出目标(X window系统、PNG图片、OpenGL等)编写的,因此用户空间坐标和设备空间坐标存在一些区别。默认情况下这两个坐标系是一对一的,这样可以将整数值大致映射为屏幕上的像素值,不过有需要时可以通过Cairo::Context::scale()函数对此行为进行调整。例如:你可能会需要使一个窗口的宽和高在调整比例后的坐标系中取值范围均处于0到1之间(单位正方形)。

16.2.1. 示例

在这个示例中,我们将构建一个很小但功能齐全的gtkmm程序,并将一些线绘制到窗口中。使用函数Cairo::Context::move_to()Cairo::Context::line_to()创建一个路径。move_to()函数的行为类似于将笔从纸上抬起并放到其他地方 -- 在你移动到的点和你之前所在的点直接不会画出一条线。使用line_to()函数可以在两点之间画线。

创建完你的路径以后。你依旧没有画任何看得见的东西。要想让路径可见,你需要使用stroke()函数,该函数将使用你的Cairo::Context对象中保存的线宽与样式绘制当前的路径。描绘完成后,当前路径将被清除,以便你可以从下一个路径开始。

多数的开罗绘图函数都拥有一个_preserve()变体。通常情况下clip()fill()stroke()函数会清除当前的路径。如果你使用他们的_preserve()变体,则当前路径会被保留,以便下一个绘图函数能继续在一样的路径上绘制。

Figure 16-1绘图区域 - 线

源代码

File: myarea.h (For use with gtkmm 4)

#ifndef GTKMM_EXAMPLE_MYAREA_H
#define GTKMM_EXAMPLE_MYAREA_H

#include <gtkmm/drawingarea.h>

class MyArea : public Gtk::DrawingArea
{
public:
  MyArea();
  virtual ~MyArea();

protected:
  void on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height);
};

#endif // GTKMM_EXAMPLE_MYAREA_H

File: main.cc (For use with gtkmm 4)

#include "myarea.h"
#include <gtkmm/application.h>
#include <gtkmm/window.h>

class ExampleWindow : public Gtk::Window
{
public:
  ExampleWindow();

protected:
  MyArea m_area;
};

ExampleWindow::ExampleWindow()
{
  set_title("DrawingArea");
  set_child(m_area);
}

int main(int argc, char** argv)
{
  auto app = Gtk::Application::create("org.gtkmm.example");

  return app->make_window_and_run<ExampleWindow>(argc, argv);
}

File: myarea.cc (For use with gtkmm 4)

#include "myarea.h"
#include <cairomm/context.h>

MyArea::MyArea()
{
  set_draw_func(sigc::mem_fun(*this, &MyArea::on_draw));
}

MyArea::~MyArea()
{
}

void MyArea::on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height)
{
  // coordinates for the center of the window
  int xc, yc;
  xc = width / 2;
  yc = height / 2;

  cr->set_line_width(10.0);

  // draw red lines out from the center of the window
  cr->set_source_rgb(0.8, 0.0, 0.0);
  cr->move_to(0, 0);
  cr->line_to(xc, yc);
  cr->line_to(0, height);
  cr->move_to(xc, yc);
  cr->line_to(width, yc);
  cr->stroke();
}

这个程序包含一个MyArea类,它是Gtk::DrawingArea的子类,并且包含on_draw()成员函数。通过在MyArea的构造函数中调用set_draw_func()使其变成绘制函数。每当需要重绘绘图区域的图像的时候,on_draw()就会被调用。这将会向我们的绘制函数传递一个指向Cairo::ContextCairo::RefPtr指针。实际进行绘图的代码使用set_source_rgb()设置我们将用于绘图的颜色,这个函数接受定义颜色所需的红色、绿色、蓝色分量作为参数(参数的有效值介于0到1之间)。设置完颜色后,我们使用move_to()line_to()函数创建一个新的路径,然后使用stroke()绘制了这个路径。

使用相对坐标绘图

在上面的例子中,我们使用绝对坐标进行绘图。你也可以使用相对坐标进行绘图。对于直线而言,你可以通过Cairo::Context::rel_line_to()完成用相对坐标绘图。

16.2.2. 线型

除了绘制基本直线以外,你还可以对线条进行很多自定义。你已经看到了对线条颜色和线条宽度的示例,但还有其他示例。

如果你于一个路径绘制了一系列的线条,你可能希望它们以某种方式被连接起来。开罗提供了三种不同的连接线条的方式:斜接(Miter)、斜切(Bevel)、圆(Round)。这些连接方式的示意图如下:

Figure 16-2开罗的不同连接类型

线条连接样式由Cairo::Context::set_line_join()函数进行设置。

线尾也有不同的样式。默认样式是从线条的开始到结尾准确的停止。这被叫做平端。其他的可选性有圆(使用圆形结束,以终点为圆心)和方(使用正方形结束,以终点为正方形的中心)。这可以使用Cairo::Context::set_line_cap()函数使用。

你还可以自定义其他内容,比如创建虚线等。更多有关信息请参阅开罗API文档。

16.2.3. 画细线

如果你尝试画一条宽度为一个像素的线条,你可能会注意到这条线有时会变得模糊并且比应有的宽度更宽。发生这种情况的原因是开若尝试在所选位置向两边绘制(一边一半),因此如果你如果正好位于像素交点上,还想绘制一个像素宽度的线条,那是不可能做到的(像素是最小的单位)。所以当像素的宽度是奇数的时候就会出现这种情况(不只是一个像素的时候)。

解决问题的诀窍是将像素放于要绘制的线条的中间,从而确保获得所需的结果。请参阅开罗常见问题

Figure 16-3绘图区域 - 细线

源代码

File: examplewindow.h (For use with gtkmm 4)

#ifndef GTKMM_EXAMPLEWINDOW_H
#define GTKMM_EXAMPLEWINDOW_H

#include <gtkmm/window.h>
#include <gtkmm/box.h>
#include <gtkmm/checkbutton.h>
#include "myarea.h"

class ExampleWindow : public Gtk::Window
{
public:
  ExampleWindow();
  virtual ~ExampleWindow();

protected:
  //Signal handlers:
  void on_button_toggled();

private:
  Gtk::Box m_HBox;
  MyArea m_Area_Lines;
  Gtk::CheckButton m_Button_FixLines;
};

#endif //GTKMM_EXAMPLEWINDOW_H

File: myarea.h (For use with gtkmm 4)

#ifndef GTKMM_EXAMPLE_MYAREA_H
#define GTKMM_EXAMPLE_MYAREA_H

#include <gtkmm/drawingarea.h>

class MyArea : public Gtk::DrawingArea
{
public:
  MyArea();
  virtual ~MyArea();

  void fix_lines(bool fix = true);

protected:
  void on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height);

private:
  double m_fix;
};

#endif // GTKMM_EXAMPLE_MYAREA_H

File: main.cc (For use with gtkmm 4)

#include "examplewindow.h"
#include <gtkmm/application.h>

int main(int argc, char* argv[])
{
  auto app = Gtk::Application::create("org.gtkmm.example");

  //Shows the window and returns when it is closed.
  return app->make_window_and_run<ExampleWindow>(argc, argv);
}

File: examplewindow.cc (For use with gtkmm 4)

#include "examplewindow.h"

ExampleWindow::ExampleWindow()
: m_HBox(Gtk::Orientation::HORIZONTAL),
  m_Button_FixLines("Fix lines")
{
  set_title("Thin lines example");

  m_HBox.append(m_Area_Lines);
  m_HBox.append(m_Button_FixLines);

  set_child(m_HBox);

  m_Button_FixLines.signal_toggled().connect(
    sigc::mem_fun(*this, &ExampleWindow::on_button_toggled));

  // Synchonize the drawing in m_Area_Lines with the state of the toggle button.
  on_button_toggled();
}

ExampleWindow::~ExampleWindow()
{
}

void ExampleWindow::on_button_toggled()
{
  m_Area_Lines.fix_lines(m_Button_FixLines.get_active());
}

File: myarea.cc (For use with gtkmm 4)

#include "myarea.h"

MyArea::MyArea()
: m_fix (0)
{
  set_content_width(200);
  set_content_height(100);
  set_draw_func(sigc::mem_fun(*this, &MyArea::on_draw));
}

MyArea::~MyArea()
{
}

void MyArea::on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height)
{
  cr->set_line_width(1.0);

  // draw one line, every two pixels
  // without the 'fix', you won't notice any space between the lines,
  // since each one will occupy two pixels (width)
  for (int i = 0; i < width; i += 2)
  {
    cr->move_to(i + m_fix, 0);
    cr->line_to(i + m_fix, height);
  }

  cr->stroke();
}

// Toogle between both values (0 or 0.5)
void MyArea::fix_lines(bool fix)
{
  // to get the width right, we have to draw in the middle of the pixel
  m_fix = fix ? 0.5 : 0.0;

  // force the redraw of the image
  queue_draw();
}