添加XInput支持

现在可以买到很便宜的输入设备,如手写板,用它绘图很方便。它可以用于代替鼠标,但这样失去了这个设备的许多优点:

关于XInput扩展的更多信息请参见XInput HOWTO

我们看GdkEventMotion结构的完全定义,我们会发现它包含支持扩展设备信息的域。

struct _GdkEventMotion
{
  GdkEventType type;
  GdkWindow *window;
  guint32 time;
  gdouble x;
  gdouble y;
  gdouble pressure;
  gdouble xtilt;
  gdouble ytilt;
  guint state;
  gint16 is_hint;
  GdkInputSource source;
  guint32 deviceid;
};

pressure是压力,0到1之间的浮点值。xtiltytilt可以取-1到1之间的值,表征在每个方向的倾斜度数。sourcedeviceid用不同的方法指出发生事件的设备。source给出设备的简短信息。它可以取如下枚举值:

GDK_SOURCE_MOUSE
GDK_SOURCE_PEN
GDK_SOURCE_ERASER
GDK_SOURCE_CURSOR

deviceid是设备的统一数字ID。它可用于得到设备的进一步信息,通过调用函数gdk_input_list_devices()。特殊值GDK_CORE_POINTER用于主要指点设备。(通常是鼠标)

允许扩展设备信息

为了让 GTK 知道我们对扩展设备信息感兴趣,我们只需添加如下一行:

gtk_widget_set_extension_events (drawing_area, GDK_EXTENSION_EVENTS_CURSOR);

GDK_EXTENSION_EVENTS_CURSOR说明我们对扩展事件感兴趣,且不想绘制自己的光标。关于绘制光标内容详见进一步讲解。我们也可以给出值GDK_EXTENSION_EVENTS_ALL,如果我们想绘制自己的光标。 或给出值GDK_EXTENSION_EVENTS_NONE反转默认条件。

然而这还没有完,默认,扩展设备是不允许的。我们需要一个机制让用户去允许和配置扩展设备。下面的程序处理一个InputDialog构件。

void
input_dialog_destroy (GtkWidget *w, gpointer data)
{
  *((GtkWidget **)data) = NULL;
}

void
create_input_dialog ()
{
  static GtkWidget *inputd = NULL;

  if (!inputd)
    {
      inputd = gtk_input_dialog_new();

      gtk_signal_connect (GTK_OBJECT(inputd), "destroy",
			  (GtkSignalFunc)input_dialog_destroy, &inputd);
      gtk_signal_connect_object (GTK_OBJECT(GTK_INPUT_DIALOG(inputd)->close_button),
				 "clicked",
				 (GtkSignalFunc)gtk_widget_hide,
				 GTK_OBJECT(inputd));
      gtk_widget_hide ( GTK_INPUT_DIALOG(inputd)->save_button);

      gtk_widget_show (inputd);
    }
  else
    {
      if (!GTK_WIDGET_MAPPED(inputd))
	gtk_widget_show(inputd);
      else
	gdk_window_raise(inputd->window);
    }
}

InputDialog有两个按钮"关闭"和"保存",默认它们没有被指定动作。在上面的函数,我们用"关闭"隐藏对话框,隐藏"保存"按钮,因为我们在这个程序里不用它。

使用扩展设备信息

一旦我们允许了这个设备,我们就能在事件结构中的额外域使用扩展设备信息。事实上,总是可以安全的使用这个信息,因为这些域值是合法的,甚至在扩展事件不允许时。

一旦改变,我们必须调用函数gdk_input_window_get_pointer()代替gdk_window_get_pointer。这是必要的,因为函数gdk_window_get_pointer不返回扩展设备信息。

void gdk_input_window_get_pointer( GdkWindow       *window,
                                   guint32         deviceid,
                                   gdouble         *x,
                                   gdouble         *y,
                                   gdouble         *pressure,
                                   gdouble         *xtilt,
                                   gdouble         *ytilt,
                                   GdkModifierType *mask);

当我们调用这个函数时,我们需要指定设备ID和窗口。通常,我们会从一个事件结构的deviceid域得到设备ID。当扩展事件不允许时,这个函数也会返回合法的值。(这样event->deviceidGDK_CORE_POINTER)。

因此我们的按钮按下和鼠标移动事件处理函数的基本结构不需要改变,我们只需要添加处理扩展信息的代码。

static gint
button_press_event (GtkWidget *widget, GdkEventButton *event)
{
  print_button_press (event->deviceid);
  
  if (event->button == 1 && pixmap != NULL)
    draw_brush (widget, event->source, event->x, event->y, event->pressure);

  return TRUE;
}

static gint
motion_notify_event (GtkWidget *widget, GdkEventMotion *event)
{
  gdouble x, y;
  gdouble pressure;
  GdkModifierType state;

  if (event->is_hint)
    gdk_input_window_get_pointer (event->window, event->deviceid,
				  &x, &y, &pressure, NULL, NULL, &state);
  else
    {
      x = event->x;
      y = event->y;
      pressure = event->pressure;
      state = event->state;
    }
    
  if (state & GDK_BUTTON1_MASK && pixmap != NULL)
    draw_brush (widget, event->source, x, y, pressure);
  
  return TRUE;
}

我们也需要对新的信息做些事。我们的新的draw_brush()函数根据每一个event->source绘制不同的颜色,依据压力改变画刷的大小。

/* 在屏幕上画一个矩形,大小依据压力,颜色依据设备的类型 */
static void
draw_brush (GtkWidget *widget, GdkInputSource source,
	    gdouble x, gdouble y, gdouble pressure)
{
  GdkGC *gc;
  GdkRectangle update_rect;

  switch (source)
    {
    case GDK_SOURCE_MOUSE:
      gc = widget->style->dark_gc[GTK_WIDGET_STATE (widget)];
      break;
    case GDK_SOURCE_PEN:
      gc = widget->style->black_gc;
      break;
    case GDK_SOURCE_ERASER:
      gc = widget->style->white_gc;
      break;
    default:
      gc = widget->style->light_gc[GTK_WIDGET_STATE (widget)];
    }

  update_rect.x = x - 10 * pressure;
  update_rect.y = y - 10 * pressure;
  update_rect.width = 20 * pressure;
  update_rect.height = 20 * pressure;
  gdk_draw_rectangle (pixmap, gc, TRUE,
		      update_rect.x, update_rect.y,
		      update_rect.width, update_rect.height);
  gtk_widget_draw (widget, &update_rect);
}

得到更多关于设备的信息

作为一个如何得到更多关于设备的信息的示例,我们的程序在每次按钮按下时打印设备名。用如下函数可以得到设备名:

GList *gdk_input_list_devices               (void);

返回值是一个GdkDeviceInfo结构的GList(GLib库的一个链表类型)GdkDeviceInfo结构定义如下:

struct _GdkDeviceInfo
{
  guint32 deviceid;
  gchar *name;
  GdkInputSource source;
  GdkInputMode mode;
  gint has_cursor;
  gint num_axes;
  GdkAxisUse *axes;
  gint num_keys;
  GdkDeviceKey *keys;
};

这些域的大部分都是可以忽略的配置信息,除非你要实现XInput配置保存。我们感兴趣的域是name,它是X分配给设备的名子。其它的不是配置信息的域是has_cursor。如果has_cursor是 FALSE,我们需要自绘制光标。但因为我们已经指定了GDK_EXTENSION_EVENTS_CURSOR,所以我们不必关心这个。

函数print_button_press()简单的重复,直到找到匹配,然后打印出设备名。

static void
print_button_press (guint32 deviceid)
{
  GList *tmp_list;

  /* gdk_input_list_devices返回一个内部列表,因此我们后面不必释放它。*/
  tmp_list = gdk_input_list_devices();

  while (tmp_list)
    {
      GdkDeviceInfo *info = (GdkDeviceInfo *)tmp_list->data;

      if (info->deviceid == deviceid)
	{
	  printf("Button press on device '%s'\n", info->name);
	  return;
	}

      tmp_list = tmp_list->next;
    }
}

我们的程序已经完全添加了对XInput设备的支持。

进一步的讲解

虽然我们的程序已经很好的支持了XInput,但是它缺乏一些特性,我们想让它成为一个全功能的程序。首先,用户不想每次在程序运行时配置设备,因此我们应该允许用户保存设备配置。这是通过获取gdk_input_list_devices()的返回值,并把配置写入一个文件。

为了程序下次运行时恢复状态,GDK 提供修改设备配置的函数:

gdk_input_set_extension_events()
gdk_input_set_source()
gdk_input_set_mode()
gdk_input_set_axes()
gdk_input_set_key()

(gdk_input_list_devices()返回的列表不能直接修改。)在绘图程序gsumi中可以发现它的用法。(http://www.msc.cornell.edu/~otaylor/gsumi/)其实做这个,最好使用所有程序标准的方法。这也许属于比 GTK 稍高级的库,也许在 GNOME 库中。

另一个缺点是我们上面提到的,缺乏光标绘制。当前平台 XFree86 不允许同时用一个设备和主指点设备在一个应用程序中。详见XInput-HOWTO。更好的应用程序应该绘制自己的光标。

一个程序要绘制自己的光标,需要两方面:确定当前设备是否需要绘制光标,确定当前设备是否"in proximity"。(如果当前设备是手写板,最好在笔尖离开平板时不显示光标。当设备是触摸板时,那叫做"in proximity"。)首先要搜索设备列表,寻找设备名。其次是选择 "proximity_out" 事件。它的用法见 GTK 发布中的示例程序"testinput".