Event Handling
The GTK signals we have already discussed are for
high-level actions, such as a menu item being selected. However,
sometimes it is useful to learn about lower-level occurrences, such as
the mouse being moved, or a key being pressed. There are also GTK
signals corresponding to these low-level events. The handlers for these
signals have a parameter which is a reference to a class containing
information about the event. For instance, motion event handlers are
passed a reference to a Gdk::EventMotion class which looks like:
class
EventMotion : public EventAny
{
public:
// Accessors
GdkEventMotion* gdk_event_motion() const { return
(GdkEventMotion*)this; }
unsigned int time()
const;
double x() const;
double y() const;
double* axes() const;
unsigned int state()
const;
bool is_hint() const;
Device* device() const;
double x_root() const;
double y_root() const;
}; |
All events inherit the following methods:
Gdk::EventType type() const;
Gdk::Window* window() const; |
type() returns the type of event, in this case
Gdk::MOTION_NOTIFY. window() returns the Gdk::Window in which the event
occurred. x() and y() return the coordinates of the event and state()
specifies the modifier state when the event occurred (that is, it
specifies which modifier keys and mouse buttons were pressed). It is the
bitwise OR of one or more of the following values in the
Gdk::ModifierType enumeration:
- SHIFT_MASK
- LOCK_MASK
- CONTROL_MASK
- MOD1_MASK
- MOD2_MASK
- MOD3_MASK
- MOD4_MASK
- MOD5_MASK
- BUTTON1_MASK
- BUTTON2_MASK
- BUTTON3_MASK
- BUTTON4_MASK
- BUTTON5_MASK
- RELEASE_MASK
- MODIFIER_MASK
As for other signals, to determine what happens
when an event occurs we can either explicitly connect a signal handler
to a signal or we can derive a new widget class and override the virtual
signal handlers for the events we are interested in. We also need let
GTK know which events we want to be notified about. To do this, we call
the Gtk::Widget method:
void
set_events(Gdk::EventMaskField events);
|
where events is the bitwise OR of one or
more of the following values in the Gdk::EventMask enumeration that
specify the event types:
- EXPOSURE_MASK
- POINTER_MOTION_MASK
- POINTER_MOTION_HINT_MASK
- BUTTON_MOTION_MASK
- BUTTON1_MOTION_MASK
- BUTTON2_MOTION_MASK
- BUTTON3_MOTION_MASK
- BUTTON_PRESS_MASK
- BUTTON_RELEASE_MASK
- KEY_PRESS_MASK
- KEY_RELEASE_MASK
- ENTER_NOTIFY_MASK
- LEAVE_NOTIFY_MASK
- FOCUS_CHANGE_MASK
- STRUCTURE_MASK
- PROPERTY_CHANGE_MASK
- VISIBILITY_NOTIFY_MASK
- ROXIMITY_IN_MASK
- PROXIMITY_OUT_MASK
- SUBSTRUCTURE_MASK
- SCROLL_MASK
- ALL_EVENTS_MASK
There are a few subtle points that have to be
observed when calling Gtk::Widget::set_events(). First, it must be
called before the X window for a widget is created. In practical terms,
this means you should call it immediately after creating the widget.
Second, the widget must have an associated X window. For efficiency,
many widget types do not have their own window, but draw in their
parent's window. These widgets are:
- Gtk::Alignment
- Gtk::Arrow
- Gtk::Bin
- Gtk::Box
- Gtk::Image
- Gtk::Item
- Gtk::Label
- Gtk::Pixmap
- Gtk::ScrolledWindow
- Gtk::Separator
- Gtk::Table
- Gtk::AspectFrame
- Gtk::Frame
- Gtk::VBox
- Gtk::HBox
- Gtk::VSeparator
- Gtk::HSeparator
To capture events for these widgets, you need to
use an EventBox widget. See the section on the
EventBox
widget for details.
For our drawing program, we want to know when the
mouse button is pressed and when the mouse is moved, so we specify
Gdk::POINTER_MOTION_MASK and Gdk::BUTTON_PRESS_MASK. We also want to
know when we need to redraw our window, so we specify
Gdk::EXPOSURE_MASK. Although we want to be notified via a Configure
event when our window size changes, we don't have to specify the
corresponding Gdk::STRUCTURE_MASK flag, because it is automatically
specified for all windows.
It turns out, however, that there is a problem with just specifying
Gdk::POINTER_MOTION_MASK. This will cause the server to add a new motion
event to the event queue every time the user moves the mouse. Imagine
that it takes us 0.1 seconds to handle a motion event, but the X server
queues a new motion event every 0.05 seconds. We will soon get way
behind with the users drawing. If the user draws for 5 seconds, it will
take us another 5 seconds to catch up after they release the mouse
button! What we would like is to only get one motion event for each
event we process. The way to do this is to specify
Gdk::POINTER_MOTION_HINT_MASK.
When we specify Gdk::POINTER_MOTION_HINT_MASK, the server sends us a
motion event the first time the pointer moves after entering our window,
or after a button press or release event. Subsequent motion events will
be suppressed until we explicitly ask for the position of the pointer
using the Gdk::Window method:
Gdk::Window* get_pointer(int *x, int *y,
Gdk::ModifierTypeField *mask) const;
|
(There is another function,
Gtk::Widget::get_pointer() which has a simpler interface, but turns out
not to be very useful, since it only retrieves the position of the
mouse, not whether the buttons are pressed.)
In scribble, we derive a new DrawingArea from Gtk::DrawingArea and
override the following virtual signal handlers:
virtual
bool on_expose_event(const
Gdk::EventExpose& event);
virtual bool on_configure_event(const Gdk::EventConfigure& event);
virtual bool on_button_press_event(const Gdk::EventButton& event);
virtual bool on_motion_notify_event(const Gdk::EventMotion& event); |
We still have to let GTK know which events to notify us about. The code
that does this is:
Gdk::EventMaskField
flags = get_events();
flags |= (Gdk::LEAVE_NOTIFY_MASK | Gdk::BUTTON_PRESS_MASK |
Gdk::POINTER_MOTION_MASK | Gdk::POINTER_MOTION_HINT_MASK);
set_events(flags); |
We'll save the expose_event and configure_event
handlers for later. The motion_notify_event and button_press_event
handlers are pretty simple:
bool
DrawingArea::on_button_press_event(const
Gdk::EventButton& event)
{
if (event.button() == 1
&& pixmap)
draw_brush(event.x(), event.y());
return true;
}
bool
DrawingArea::on_motion_notify_event(const
Gdk::EventMotion& event)
{
int x, y;
Gdk::ModifierTypeField state;
if (event.is_hint())
event.window()->get_pointer(&x, &y, &state);
else
{
x = (int)event.x();
y = (int)event.y();
state = event.state();
}
if (state &
Gdk::BUTTON1_MASK && pixmap)
draw_brush(x, y);
return true;
} |
The DrawingArea
Widget, and Drawing
We now turn to the process of drawing on the
screen. The widget we use for this is the DrawingArea widget. A drawing
area widget is essentially an X window and nothing more. It is a blank
canvas in which we can draw whatever we like. A drawing area is created
using one of the following constructors:
DrawingArea();
DrawingArea(int width,
int height); |
The width and height are the
default size for the widget. If you use the first constructor you can
set the default size later by calling:
void
size(int width, int
height);
|
This default size can be overridden, as is true
for all widgets, by calling Gtk::Widget::set_size_request(), and that,
in turn, can be overridden if the user manually resizes the the window
containing the drawing area.
In this example we are going draw to our own offscreen pixmap so we can
turn off double buffering with the following method:
void set_double_buffered(bool double_buffered);
|
It should be noted that when we create a DrawingArea widget, we are
completely responsible for drawing the contents. If our window is
obscured then uncovered, we get an exposure event and must redraw what
was previously hidden.
Having to remember everything that was drawn on the screen so we can
properly redraw it can, to say the least, be a nuisance. In addition, it
can be visually distracting if portions of the window are cleared, then
redrawn step by step. The solution to this problem is to use an
offscreen backing pixmap. Instead of drawing directly to the screen, we
draw to an image stored in server memory but not displayed, then when
the image changes or new portions of the image are displayed, we copy
the relevant portions onto the screen.
To create an offscreen pixmap, we call the constructor:
Gdk::Pixmap(const Gdk::Window& window, int width, int height); |
The window parameter specifies the
Gdk::Window that this pixmap takes some of its properties from. width
and height specify the size of the pixmap.
We create the pixmap in our configure_event handler. This
event is generated whenever the window changes size, including when it
is originally created.
bool
DrawingArea::on_configure_event(const
Gdk::EventConfigure& event)
{
// Create a new backing
pixmap of the appropriate size
if (pixmap)
pixmap->unref();
pixmap = new
Gdk::Pixmap(*get_window(), get_allocation().width(),
get_allocation().height());
pixmap->draw_rectangle(*(get_style()->white_gc()), 0, 0,
get_allocation().width(), get_allocation().height());
return true;
} |
The call to Gdk::Drawable::draw_rectangle() clears
the pixmap initially to white. We'll say more about that in a moment.
Our exposure event handler then simply copies the relevant portion of
the pixmap onto the screen (we determine the area we need to redraw by
using the event.area() field of the exposure event):
bool
DrawingArea::on_expose_event(const
Gdk::EventExpose& event)
{
// Redraw the screen
from the backing pixmap
get_window()->draw_drawable(*(get_style()->fg_gc(get_state())),
*pixmap, event.area().x(), event.area().y(), event.area());
return false;
} |
We've now seen how to keep the screen up to date
with our pixmap, but how do we actually draw interesting stuff on our
pixmap? There are a large number of calls in Inti's Gdk library for
drawing on drawables. A drawable is simply something that can be drawn
upon. It can be a window, a pixmap, or a bitmap (a black and white
image). We've already seen two such calls above,
Gdk::Drawable::draw_rectangle() and Gdk::Drawable:draw_drawable().
The complete list of Gdk::Drawable methods is:
- draw_point()
- draw_line()
- draw_rectangle()
- draw_arc()
- draw_polygon()
- draw_drawable()
- draw_image()
- draw_points()
- draw_segments()
- draw_lines()
- draw_glyphs()
- draw_layout_line()
- draw_layout()
- draw_layout_line_with_colors()
See the the header file
<inti/gdk/drawable.h> for further details on these methods. These
methods all share the same first argument - a reference to a graphics
context (GC).
A graphics context encapsulates information about things such as
foreground and background color and line width. Gdk::GC has a full set
of methods for creating and modifying graphics contexts, but to keep
things simple we'll just use predefined graphics contexts. Each widget
has an associated style. (Which can be modified in a gtkrc file.) This,
among other things, stores a number of graphics contexts. Some examples
of accessing these graphics contexts are:
widget->get_style()->white_gc()
widget->get_style()->black_gc()
widget->get_style()->fg_gc(Gtk::STATE_NORMAL)
widget->get_style()->bg_gc(widget->get_state())
|
The methods fg_gc(), bg_gc(), dark_gc(), and
light_gc() take a single parameter, the Gtk::StateType, which can be one
of the values from the Gtk::StateType enumeration:
- STATE_NORMAL,
- STATE_ACTIVE,
- STATE_PRELIGHT,
- STATE_SELECTED,
- STATE_INSENSITIVE
For instance, for Gtk::STATE_SELECTED the default
foreground color is white and the default background color, dark blue.
Our draw_brush() method, which does the actual drawing on the screen,
is then:
void
DrawingArea::draw_brush(double x, double y)
{
// Draw a rectangle on
the screen
Gdk::Rectangle update_rect((int)x - 5, (int)y
- 5, 10, 10);
pixmap->draw_rectangle(*(get_style()->black_gc()),
update_rect);
queue_draw_area(update_rect);
} |
After we draw the rectangle representing the brush onto the pixmap, we
call one of the following Gtk::Widget methods:
void
queue_draw_area(int x, int y, int width, int height);
void queue_draw_area(const Gdk::Rectangle& rectangle); |
which notifies X that the area given needs to be
updated. X will eventually generate an expose event which will cause our
expose event handler to copy the relevant portions to the screen.
We have now covered the entire drawing program except for a few mundane
details like creating the main window.
The Complete
Scribble Source Code
The header file for the
scribble example is scribble.h:
#include<inti/main.h>
#include <inti/core.h>
#include <inti/gtk/drawingarea.h>
using namespace Inti;
class DrawingArea : public Gtk::DrawingArea
{
Gdk::Pixmap *pixmap;
void draw_brush(double x, double y);
protected:
virtual bool
on_expose_event(const Gdk::EventExpose&
event);
virtual bool
on_configure_event(const
Gdk::EventConfigure& event);
virtual bool
on_button_press_event(const Gdk::EventButton&
event);
virtual bool
on_motion_notify_event(const
Gdk::EventMotion& event);
public:
DrawingArea();
virtual ~DrawingArea();
};
class ScribbleWindow : public Gtk::Window
{
public:
ScribbleWindow();
virtual
~ScribbleWindow();
}; |
and the source file is scribble.cc:
#include"scribble.h"
#include <inti/gtk/style.h>
#include <inti/gtk/button.h>
#include <inti/gdk/color.h>
#include <inti/gdk/gc.h>
#include <inti/gdk/pixmap.h>
#include <inti/gdk/window.h>
// DrawingArea
DrawingArea::DrawingArea()
: pixmap(0)
{
set_size_request(200, 200);
set_double_buffered(false);
Gdk::EventMaskField flags = get_events();
flags |= (Gdk::LEAVE_NOTIFY_MASK |
Gdk::BUTTON_PRESS_MASK | Gdk::POINTER_MOTION_MASK |
Gdk::POINTER_MOTION_HINT_MASK);
set_events(flags);
}
DrawingArea::~DrawingArea()
{
if (pixmap)
pixmap->unref();
}
bool
DrawingArea::on_expose_event(const
Gdk::EventExpose& event)
{
// Redraw the screen
from the backing pixmap
get_window()->draw_drawable(*(get_style()->fg_gc(get_state())),
*pixmap, event.area().x(), event.area().y(), event.area());
return false;
}
bool
DrawingArea::on_configure_event(const
Gdk::EventConfigure& event)
{
// Create a new backing
pixmap of the appropriate size
if (pixmap)
pixmap->unref();
pixmap = new
Gdk::Pixmap(*get_window(), get_allocation().width(),
get_allocation().height());
pixmap->draw_rectangle(*(get_style()->white_gc()), 0, 0,
get_allocation().width(), get_allocation().height());
return true;
}
bool
DrawingArea::on_button_press_event(const
Gdk::EventButton& event)
{
if (event.button() == 1
&& pixmap)
draw_brush(event.x(), event.y());
return true;
}
bool
DrawingArea::on_motion_notify_event(const
Gdk::EventMotion& event)
{
int x, y;
Gdk::ModifierTypeField state;
if (event.is_hint())
event.window()->get_pointer(&x, &y, &state);
else
{
x = (int)event.x();
y = (int)event.y();
state = event.state();
}
if (state &
Gdk::BUTTON1_MASK && pixmap)
draw_brush(x, y);
return true;
}
void
DrawingArea::draw_brush(double x, double y)
{
// Draw a rectangle on
the screen
Gdk::Rectangle update_rect((int)x - 5, (int)y
- 5, 10, 10);
pixmap->draw_rectangle(*(get_style()->black_gc()), update_rect);
queue_draw_area(update_rect);
}
// ScribbleWindow
ScribbleWindow::ScribbleWindow()
{
set_name("Test Input");
Gtk::VBox *vbox = new
Gtk::VBox;
add(*vbox);
vbox->show();
// Create the drawing
area
DrawingArea *drawing_area = new DrawingArea;
vbox->pack_start(*drawing_area);
drawing_area->show();
// And a quit button
Gtk::Button *button = new Gtk::Button("Quit");
vbox->pack_start(*button, false,false);
button->sig_clicked().connect(slot(this, &ScribbleWindow::dispose));
button->show();
}
ScribbleWindow::~ScribbleWindow()
{
}
int main (int
argc, char *argv[])
{
using namespace Main;
init(&argc, &argv);
ScribbleWindow window;
window.sig_destroy().connect(slot(&Inti::Main::quit));
window.show();
run();
return 0;
}
|