アプリケーションの例: Cairo時計の製作

さて、Cairoによる描画の基本はカバーしました。それではこれらの知識を全て投入してシンプルですが実用的なアプリケーションを製作してみましょう。以下のコード例では、Cairoを使ってカスタム時計(Clock)ウィジットを作成しています。時計には秒針、分針、時針があり、毎秒ごとに更新されます。

Source Code

File: clock.h

#ifndef GTKMM_EXAMPLE_CLOCK_H
#define GTKMM_EXAMPLE_CLOCK_H

#include <gtkmm/drawingarea.h>

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

    protected:
        //Override default signal handler:
        virtual bool on_expose_event(GdkEventExpose* event);
        double m_radius;
        double m_lineWidth;
        bool onSecondElapsed(void);
};

#endif // GTKMM_EXAMPLE_CLOCK_H

File: main.cc

#include "clock.h"
#include <gtkmm/main.h>
#include <gtkmm/window.h>

int main(int argc, char** argv)
{
   Gtk::Main kit(argc, argv);

   Gtk::Window win;
   win.set_title("Cairomm Clock");

   Clock c;
   win.add(c);
   c.show();

   Gtk::Main::run(win);

   return 0;
}

File: clock.cc

#include <ctime>
#include <cairomm/context.h>
#include "clock.h"

Clock::Clock()
    : m_radius(0.42), m_lineWidth(0.05)
{
    Glib::signal_timeout().connect(
                sigc::mem_fun(*this, &Clock::onSecondElapsed), 1000);
}

Clock::~Clock()
{
}

bool Clock::on_expose_event(GdkEventExpose* event)
{
  // This is where we draw on the window
  Glib::RefPtr<Gdk::Window> window = get_window();
  if(window)
  {
    Gtk::Allocation allocation = get_allocation();
    const int width = allocation.get_width();
    const int height = allocation.get_height();

    Cairo::RefPtr<Cairo::Context> cr = window->create_cairo_context();

    if (event)
    {
        // clip to the area indicated by the expose event so that we only
        // redraw the portion of the window that needs to be redrawn
        cr->rectangle(event->area.x, event->area.y,
                event->area.width, event->area.height);
        cr->clip();
    }

    // scale to unit square and translate (0, 0) to be (0.5, 0.5), i.e.
    // the center of the window
    cr->scale(width, height);
    cr->translate(0.5, 0.5);
    cr->set_line_width(m_lineWidth);

    cr->save();
    cr->set_source_rgba(0.337, 0.612, 0.117, 0.9);   // green
    cr->paint();
    cr->restore();
    cr->arc(0, 0, m_radius, 0, 2 * M_PI);
    cr->save();
    cr->set_source_rgba(1.0, 1.0, 1.0, 0.8);
    cr->fill_preserve();
    cr->restore();
    cr->stroke_preserve();
    cr->clip();

    //clock ticks
    for (int i = 0; i < 12; i++)
    {
        double inset = 0.05;

        cr->save();
        cr->set_line_cap(Cairo::LINE_CAP_ROUND);

        if (i % 3 != 0)
        {
            inset *= 0.8;
            cr->set_line_width(0.03);
        }

        cr->move_to(
                (m_radius - inset) * cos (i * M_PI / 6),
                (m_radius - inset) * sin (i * M_PI / 6));
        cr->line_to (
                m_radius * cos (i * M_PI / 6),
                m_radius * sin (i * M_PI / 6));
        cr->stroke();
        cr->restore(); /* stack-pen-size */
    }

    // store the current time
    time_t rawtime;
    time(&rawtime);
    struct tm * timeinfo = localtime (&rawtime);

    // compute the angles of the indicators of our clock
    double minutes = timeinfo->tm_min * M_PI / 30;
    double hours = timeinfo->tm_hour * M_PI / 6;
    double seconds= timeinfo->tm_sec * M_PI / 30;

    cr->save();
    cr->set_line_cap(Cairo::LINE_CAP_ROUND);

    // draw the seconds hand
    cr->save();
    cr->set_line_width(m_lineWidth / 3);
    cr->set_source_rgba(0.7, 0.7, 0.7, 0.8); // gray
    cr->move_to(0, 0);
    cr->line_to(sin(seconds) * (m_radius * 0.9), 
            -cos(seconds) * (m_radius * 0.9));
    cr->stroke();
    cr->restore();

    // draw the minutes hand
    cr->set_source_rgba(0.117, 0.337, 0.612, 0.9);   // blue
    cr->move_to(0, 0);
    cr->line_to(sin(minutes + seconds / 60) * (m_radius * 0.8),
            -cos(minutes + seconds / 60) * (m_radius * 0.8));
    cr->stroke();

    // draw the hours hand
    cr->set_source_rgba(0.337, 0.612, 0.117, 0.9);   // green
    cr->move_to(0, 0);
    cr->line_to(sin(hours + minutes / 12.0) * (m_radius * 0.5),
            -cos(hours + minutes / 12.0) * (m_radius * 0.5));
    cr->stroke();
    cr->restore();

    // draw a little dot in the middle
    cr->arc(0, 0, m_lineWidth / 3.0, 0, 2 * M_PI);
    cr->fill();

  }

  return true;
}


bool Clock::onSecondElapsed(void)
{
    // force our program to redraw the entire clock.
    Glib::RefPtr<Gdk::Window> win = get_window();
    if (win)
    {
        Gdk::Rectangle r(0, 0, get_allocation().get_width(),
                get_allocation().get_height());
        win->invalidate_rect(r, false);
    }
    return true;
}

以前の例と同様、肝心なところは全てexposeイベントハンドラ on_expose_event() の中で行われます。ですがexposeイベントハンドラに入る前に、注目してほしいのはClockウィジットのコンストラクタがハンドラ関数onSecondElapsed()を1000ミリ秒(1秒)のタイムアウトを持つタイマーに接続しているところです。これはつまり、onSecondElapsed()関数を1秒ごとに呼び出すということを意味します。この関数の唯一の役割はウィンドウの変更を検証しないようにすることですから、結果としてgtkmmはウィンドウを強制的に再描画します。

さて、描画を実際に担当するコードを見ていきましょう。on_expose_event()の始めの部分は既にもう馴じみ深い'鉄板型'のコードでしょう。Gdk::Windowを取得し、Cairo::Contextを生成、再描画に使う領域をクリッピングしています。この例ではさらに、座標系を単位格子のサイズにスケールしています。こうすることで時計の描画をウィンドウサイズのパーセンテージを使って簡単に指定することができ、ウィンドウサイズを変更しても自動的に追随するようにできます。最後に、座標系を調整することで(0,0)をウィンドウの中心に持ってきます。

ここで使われているCairo::Context::paint()はウィンドウの背景色を設定しています。この関数は引数を取らず、現在のサーフィスを(あるいはクリッピングしたその一部分を)現在有効になっている色で塗りつぶします。ウィンドウの背景色の設定が済んだら円で時計の大枠を描きます。白で塗りつぶし、それから黒でストロークをなぞります。注目してほしいのは、これらの動作には共に_preserve変種が使用されているということです。パスを保存しておいて描画後にクリッピングし、次に描くラインが時計の外側にはみ出さないようにしています。  

大枠が描けたら、時計の回りに時刻を示す線を描きます。一時間ごとに一本、12, 3, 6, 9時には長いものです。さて、最後にやっと現在の時刻を示す機能を実装しましょう。これは単に現在の時刻、分、秒を取得し、正しい角度で時針を描いてやればいいだけです。