FoundationsofGTKDevelopment:Chapter 11
- 제 11 장 커스텀 위젯 생성하기
커스텀 위젯 생성하기
지금까지 GTK+와 그것이 지원하는 라이브러리에 대해 많은 내용을 배웠다. 이제 GTK+가 제공하는 위젯을이용해 자신만의 복잡한 애플리케이션을 생성하는 방법에 대한 지식은 충분히 습득하였을 것이다.
하지만 아직 학습하지 않은 주제가 하나 있는데, 바로 자신만의 위젯을 생성하는 방법이다. 따라서 이번 장에서는 GObject에서 새로운 클래스를 상속하는 데에 주력하겠다. 세 가지 예를 통해 안내하겠다.
첫 번째 예는 GtkEntry 위젯에서 MyIPAddress라고 불리는 새로운 위젯을 상속한다. 이 위젯은 사용자가 IP 주소를 입력하여 그에 따라 커서의 위치를 제어하도록 해준다. 두 번째 예제에서는 MyMarquee라고 불리는 새로운 커스텀 GtkWidget 클래스를 생성할 것인데, 이는 메시지를 명시된 속도로 스크롤 시 이용된다. 마지막으로 커스텀 인터페이스를 구현하고 이용하는 방법을 학습할 것이다.
이번 장에서 배울 내용은 다음과 같다.
- 이미 존재하는 클래스와 위젯에서 새로운 클래스 및 위젯을 상속하는 방법
- GtkWidget에서 상속된 커스텀 위젯을 생성하는 방법. 이는 개발자 스스로 화면에 위젯을 노출시키고 그리도록 요한다.
- 커스텀 인터페이스를 구현하고 사용하는 방법.
새로운 위젯 상속하기
이번 장은 이미 존재하는 타입에서 새로운 GObject 타입을 생성하는 방법을 가르치는 데에 목표를 둔다. 새로운 객체의 상속 방법을 가장 잘 학습하는 방법은 예를 이용하는 것이다. 따라서 이번 절에서는 사용자가 IP 주소를 입력할 수 있는 MyIPAddress라고 불리는 새로운 위젯을 생성할 것이다. 이 위젯은 사용자가 유효한 IP 주소 외에는 입력하지 못하도록 제한한다.
이번 절에서 새로운 위젯을 상속하면서 사용한 방식은 GObject에서 상속된 모든 객체에 적용된다. 따라서 새로운 위젯의 상속으로 국한되지 않고 GObject에서 직접적으로 혹은 간접적으로 상속된 타입에서 새로운 타입을 상속할 수도 있다.
이번 절에서 생성한 GtkIPAddress 위젯의 스크린샷을 그림 11-1에 실었다. 한 자리수와 두 자리수 모두 우측으로 정렬되어 있다.
MyIPAddress 헤더 파일 생성하기
어떤 타입의 GObject를 상속하든 가장 먼저 할 일은 헤더 파일을 생성하는 일이다. 이 파일은 각 객체에서 필요로 하는 기본 함수의 호출을 개발자가 준비하도록 해준다. 헤더 파일은 개발자의 새로운 객체를 이용하는 코드라면 어디서든 이용 가능한 public 함수를 포함하기 때문에 위젯을 계획 시 헤더 파일의 생성부터 시작하면 되겠다.
C++ 컴파일러를 제공하려면 G_BEGIN_DECLS와 G_END_DECLS를 이용해 헤더 파일의 내용에 괄호를 둘러야 한다. 이 두 개의 매크로는 파일 내용 주변에 extern "C"를 추가하여 C에서와 마찬가지로 컴파일 시 모든 함수들이 식별명(symbol name)을 이용하도록 강요한다. 헤더 파일의 셸에 대한 예제는 리스팅 11-1에서 찾을 수 있다.
리스팅 11-1. MyIPAddress 헤더 파일 (myipaddress.h)
#ifndef __MY_IP_ADDRESS_H__
#define __MY_IP_ADDRESS_H__
#include <glib.h>
#include <glib-object.h>
#include <gtk/gtkentry.h>
G_BEGIN_DECLS
...
G_END_DECLS
#endif /* __MY_IP_ADDRESS_H__ */
이번 책에 실린 대부분의 예제와 달리 이번 장에 실린 예제는 여러 줄로 이어지기 때문에 전체를 표시하지 않고 작게 나누어 표시하였다. 소스 코드의 전체 파일은 본 서적의 웹 사이트 www.gtkbook.com에서 다운로드할 수 있다.
개발자는 필요한 구조체와 함수를 비롯해 새로운 위젯을 생성할 때마다 리스팅 11-2에서 보이는 바와 같이 다섯 개의 매크로를 정의할 필요가 있다. 헤더 파일의 나머지 부분에 포함된 모든 함수와 구조체 선언은 G_BEGIN_DECLS와 G_END_DECLS 사이에 위치해야 한다. 리스팅 11-2의 매크로는 모든 GObject가 사용하는 표준 명명 규칙(standard naming scheme)을 따른다. 표준 명명 규칙은 객체 상속을 훨씬 간단하게 만든다.
리스팅 11-2. GObject 지시어
#define MY_IP_ADDRESS_TYPE (my_ip_address_get_type ())
#define MY_IP_ADDRESS(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
MY_IP_ADDRESS_TYPE, MyIPAddress))
#define MY_IP_ADDRESS_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), \
MY_IP_ADDRESS_TYPE, MyIPAddressClass))
#define IS_MY_IP_ADDRESS(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
MY_IP_ADDRESS_TYPE))
#define IS_MY_IP_ADDRESS_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), \
MY_IP_ADDRESS_TYPE))
새로운 객체가 사용하는 모든 함수의 앞에는 GTK_ 또는 GDK_처럼 라이브러리명이 붙은 다음에 위젯명이 따라와야 한다. 가장 먼저 정의해야 하는 매크로는 MY_IP_ADDRESS_TYPE이다. 이 매크로는 객체에 해당하는 GType 구조체를 리턴한다. my_ip_address_get_type()은 헤더 파일에서 후에 정의하도록 하겠다.
다음 매크로인 MY_IP_ADDRESS()는 객체를 MyIPAddress로 캐스팅하는 데에 사용된다. 이는 GTK_WIDGET(), GTK_ENTRY(), 또는 G_OBJECT()처럼 함수를 이용해 객체를 캐스팅하는 것과 비슷하다. G_TYPE_CHECK_INSTANCE_CAST()는 두 가지 작업을 수행한다. 우선 객체가 올바른 타입인지 검사한다. 올바른 타입이 아니면 경고가 발생한다. 올바른 타입이 맞다면 객체는 MyIPAddress 위젯으로 캐스팅되어 리턴된다.
MY_IP_ADDRESS_CLASS()는 MY_IP_ADDRESS()와 동일한 방식으로 사용되는데, 객체를 MyIPAddressClass로 캐스팅한다는 점만 다르다. 이 두 타입의 차이점은 머지않아 설명하겠다.
코드는 C++ 컴파일러를 이용해서도 컴파일될 수도 있으므로 위젯 클래스 타입을 참조하려면 항상 class 대신 klass를 사용해야 하는데, class는 C++ 키워드에 해당하기 때문이다.
마지막으로 소개할 두 가지 매크로인 IS_MY_IP_ADDRESS()와 IS_MY_IP_ADDRESS_CLASS()는 객체가 올바른 타입인지 확인하는 데에 사용된다. 객체가 명시된 타입이면 각 함수는 TRUE를 리턴할 것이다.
그 다음 단계는 MyIPAddress와 MyIPAddressClass 구조체를 정의하는 것으로, 리스팅 11-3에 내용을 싣겠다. 위젯 내용은 _MyIPAddress에서 보유한다. 새로운 위젯의 구조체에서 첫 번째 member는 항상 상속받고자 하는 타입의 인스턴스여야 한다. 리스팅에서 볼 수 있듯이 GtkEntry로부터 MyIPAddress 위젯이 상속될 것이다.
리스팅 11-3. MyIPAddress 구조체
typedef struct _MyIPAddress MyIPAddress;
typedef struct _MyIPAddressClass MyIPAddressClass;
struct _MyIPAddress
{
GtkEntry entry;
};
struct _MyIPAddressClass
{
GtkEntryClass parent_class;
void (* ip_changed) (MyIPAddress *ipaddress);
};
MyIPAddress에서 부모 객체 GtkEntry를 포인터로 정의해선 안 된다! 이를 어길 시 새로운 위젯 클래스가 부모 위젯 클래스보다 작다는 오류가 발생하고 컴파일은 실패할 것이다.
MyIPAddress의 GtkEntry 자식은 대부분 위젯의 경우와 마찬가지로 포인터가 아님을 명심하라. 이는 상속된 객체는 모든 방면에서 그것의 부모 구조체라는 사실을 증명한다. 시그널, 프로퍼티, 스타일뿐 아니라 전체 객체 자체도 상속한다. 이러한 관계는 MyIPAddressClass 구조체에서 포인터가 아닌 GtkEntryClass 객체의 선언으로 확인할 수 있다.
MyIPAddress 구조체는 GtkEntry 객체 외에는 어떤 객체도 보유하지 않는다. 위젯 구조체 GtkEntry는 본래 프로그래머가 접근해선 안 되는 private 객체를 보유하는 데에 사용되었지만 GObject는 private 프로퍼티를 구현할 수 있는 더 나은 방법을 제공하는데, 이는 소스 파일에서 다루도록 하겠다. 개발자는 위젯 구조체내에서 꼭 필요하다고 간주되는 변수를 자유롭게 배치할 수 있지만 이를 실행하기 전에는 개발자가 객체로 직접 접근성을 가져야 하는지 고려해야 한다. 기본적으로 위젯이 변수의 변경에 반응해야 할 경우 소스 파일 내 private 클래스에서 선언되어야 한다. 반응하지 않아도 된다면 public 위젯 구조체 내에 위치하는 수도 있다.
MyIPAddress 외에도 부모 클래스 타입 GtkEntryClass의 인스턴스를 포함하는 MyIPAddressClass를 정의해야 한다. 다시 말하지만 이것은 포인터 타입이 되어선 안 되는데, 부모 프로퍼티, 시그널, 함수가 개발자의 위젯 클래스로부터 상속되는 것을 허용하기 때문이다.
그 외에도 시그널에 대한 콜백 함수 프로토타입을 위젯 클래스에 정의해야 한다. 이번 예제에서는 ip-changed 시그널이 추가될 것인데, 이 시그널은 IP 주소가 성공적으로 변경되면 호출된다. 이 시그널 덕분에 개발자는 네 개의 위젯 프로퍼티에서 내용이 변경되는지 감시하지 않아도 되는데, 네 개의 숫자 각각이 자체의 위젯 프로퍼티로 정의될 것이기 때문이다.
헤더 파일을 생성하는 과정에서 마지막 단계는 개발자가 호출할 수 있는 함수에 대한 함수 프로토타입을 정의하는 일로, 리스팅 11-4에 소개하겠다. myipaddress.c 파일에서만 접근할 수 있는 private 함수들도 정의하겠다.
리스팅 11-4. 헤더 파일 함수 프로토타입
GType my_ip_address_get_type (void) G_GNUC_CONST;
GtkWidget* my_ip_address_new (void);
gchar* my_ip_address_get_address (MyIPAddress *ipaddress);
void my_ip_address_set_address (MyIPAddress *ipaddress, gint address[4]);
리스팅 11-4에 소개된 네 가지 함수는 각각 MyIPAddress와 연관된 GType을 리턴하고, 새로운 MyIPAddress 위젯을 생성하며, IP 주소를 문자열로 리턴하고, 새로운 IP 주소값을 제공한다. 이와 관련된 내용은 본 장의 뒷부분에서 함수의 구현을 다루면서 좀 더 논하겠다.
소스 파일 생성하기
헤더 파일이 완료되었다면 새로운 객체를 상속하고 MyIPAddress의 기능을 구현할 때가 되었다. 위젯에는 추적해야 하는 프로퍼티와 시그널이 많을 것인데, 이를 아래 리스팅에서 정의할 것이다.
리스팅 11-5는 MyIPAddress 소스 파일을 걸쳐 필요로 하는 값과 구조체를 다수 정의한다. 시그널과 프로퍼티 식별자를 비롯해 private 클래스들도 이에 포함된다.
리스팅 11-5. 전역적 열거와 구조체(myipaddress.c)
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#include <stdlib.h>
#include <math.h>
#include "myipaddress.h"
#define MY_IP_ADDRESS_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), \
MY_IP_ADDRESS_TYPE, MyIPAddressPrivate))
typedef struct _MyIPAddressPrivate MyIPAddressPrivate;
struct _MyIPAddressPrivate
{
guint address[4];
};
enum
{
CHANGED_SIGNAL,
LAST_SIGNAL
};
enum
{
PROP_0,
PROP_IP1,
PROP_IP2,
PROP_IP3,
PROP_IP4
};
static guint my_ip_address_signals[LAST_SIGNAL] = { 0 };
리스팅 11-5에 정의된 매크로 MY_IP_ADDRESS_GET_PRIVATE()은 현재 객체 인스턴스와 연관된 MyIPAddressPrivate 구조체를 검색한다. 이 구조체는 객체의 private 프로퍼티를 보유하는 데 사용되는데, 이러한 프로퍼티들은 각 인스턴스에 유일하다. 이번 예제에서는 MyIPAddressPrivate 가 네 개의 IP 주소값을 각각 보유한다. 이러한 값은 private하게 유지되어 해당 파일에서 정의된 함수만 수정이 가능한데, 값이 변경되면 위젯이 업데이트 되어야 하기 때문이다.
다음 단계는 위젯에 설치될 시그널과 프로퍼티를 참조하는 데에 사용될 열거를 정의하는 일이다. CHANGED_SIGNAL은 사용자가 IP 주소의 내용을 변경하거나 프로그램적으로 변경되었을 때 발생하게 될 ip-changed 시그널을 참조한다. LAST_SIGNAL은 얼마나 많은 시그널이 위젯에 설치되고 my_ip_address_signals[]에 보관되어 있는지 알아내는 데에 사용된다. 이를 마지막 열거 값으로 정의하면 향후 시그널 계수를 업데이트해야 한다는 걱정 없이 쉽게 시그널을 추가할 수 있다.
또 다른 열거는 프로퍼티 식별자를 보유한다. 개발자의 모든 프로퍼티 식별자는 선언 시 0보다 커야 하므로 PROP_0의 초기 열거 값을 위치시키는 것이 관행이다. 다른 열거 값들은 IP 주소를 구성하는 네 개의 정수를 참조한다. 이는 시그널을 위젯 클래스로 추가 시 사용된다. 새로운 위젯을 이용하는 프로그래머는 후에 개발자가 정의하는 프로퍼티명을 이용할 수 있다.
새로운 GType 등록하기
헤더 파일에서 우리는 리스팅 11-6에 구현된 my_ip_address_get_type() 함수에 대한 함수 프로토타입을 정의하였다. 이 함수는 GType 값을 리턴하는데, 이는 등록된 타입에 유일한 수치값에 불과하다. 이런 경우 등록된 타입은 MyIPAddress 객체가 된다.
리스팅 11-6. 새로운 MyIPAddress 타입 생성하기
GType
my_ip_address_get_type (void)
{
static GType entry_type = 0;
if (!entry_type)
{
static const GTypeInfo entry_info =
{
sizeof (MyIPAddressClass),
NULL,
NULL,
(GClassInitFunc) my_ip_address_class_init,
NULL,
NULL,
sizeof (MyIPAddress),
0,
(GInstanceInitFunc) my_ip_address_init,
};
entry_type = g_type_register_static (GTK_TYPE_ENTRY, "MyIPAddress",
&entry_info, 0);
}
return entry_type;
}
타입이 아직 생성되지 않았다면 객체가 초기화되는 동안 정적 식별자(static identifier)가 설정되지 않았으니 우리가 추가해야 한다는 뜻이다. 새로운 GType을 등록할 때는 먼저 타입에 대한 GTypeInfo 객체를 선언해야 한다. GTypeInfo 구조체에는 표 11-1에 정의된 10개의 member가 있는데, 모든 member가 필요한 것은 아니다.
변수 | 설명 |
guint16 class_size | 위젯을 생성 시 필요한 클래스 구조체의 크기. MyIPAddressClass 구조체의 크기일 뿐이다. |
GBaseInitFunc base_init | 기본(base) 초기화 함수의 선택적 위치. 이 콜백 함수는 부모 클래스에서 복사한 모든 동적인 클래스 member를 재할당하는 데에 사용된다. |
GBaseFinalizeFunc base_finalize | 기본 최종화(finalization) 함수의 선택적 위치. 이 콜백 함수는 GBaseInitFunc 함수에서 실행한 내용을 최종화하는 데에 사용된다. |
GClassInitFunc class_init | 클래스 초기화 함수의 선택적 구현으로, 클래스에 대한 가상 함수를 채우고 시그널 및 객체 프로퍼티를 등록하는 데에 사용된다. |
GClassFinalizeFunc class_finalize | 클래스 최종화 함수의 선택적 구현. 동적으로 할당된 자원은 GBaseInitFunc와 GBaseFinalizeFunc 함수에서 처리되어야 하므로 이 함수가 필요한 경우는 극히 드물다. |
gconstpointer class_data | GClassInitFunc와 GClassFinalizeFunc의 구현으로 전달될 포인터 데이터. |
guint16 instance_size | 상속 중인 객체 또는 위젯의 크기. MyIPAddress 구조체의 크기일 뿐이다. |
guint16 n_preallocs | GLib 2.10 배포판 이후부터 이 member는 무시되는데, 슬라이스 할당자를 이용해 인스턴스의 메모리 할당이 처리되기 때문이다. |
GInstanceInitFunc instance_init | 인스턴스를 준비하는 데에 사용되는 선택적 함수. MyIPAddress 예제에서 이 함수는 key-press-event와 changed 시그널을 각 GtkEntry로 연결하고 위젯을 패킹한다. |
const GTypeValueTable *value_table | 이 타입에 대한 일반 GValue 객체를 처리하는 함수 테이블. 기본 타입을 생성할 때에만 주로 사용되므로 대부분의 경우는 정의하지 않아도 된다. |
표 11-1. GTypeInfo members |
새로운 클래스를 초기화하는 단계는 네 가지로 나뉘는데, 부모 클래스로부터 member를 복사하고, 나머지 member를 0으로 초기화하며, GBaseInitFunc 초기화기(initializer)를 호출하고, GClassInitFunc initializer를 호출하는 것에 해당한다. 이 단계들은 객체의 새로운 인스턴스들이 인스턴스화될 때마다 실행해야 한다. GClassInitFunc는 새로운 GType이 유효해지기 위해 GTypeInfo에서 필요로 하는 함수다.
자신의 새 타입에 대해 GTypeInfo 객체를 준비했다면 다음으로 g_type_register_static()을 이용해 GType을 등록할 차례다. 이 함수의 첫 번째 매개변수는 부모 타입을 참조하는 GType 값이다. 가령 MyIPAddress로부터 객체를 상속한다면 이는 GTK_TYPE_IP_ADDRESS로 설정될 것이다. GtkEntry 위젯으로부터 새 객체를 상속하려면 GTK_TYPE_ENTRY를 이용할 수 있다.
GType g_type_register_static (GType parent_type,
const gchar *type_name,
const GTypeInfo *info,
GTypeFlags flags);
다음으로는 새 타입의 이름으로 사용될 문자열과 그에 상응하는 GTypeInfo 객체를 명시해야 한다. 우리 예제에서는 위젯의 이름인 MyIPAddress 문자열이 사용되었다. 객체에 유일해야 하기 때문에 위젯의 이름을 사용하는 것은 좋은 생각이다. 이름은 알파벳 문자로 시작해 최소 3개의 문자로 구성되어야 한다.
마지막 매개변수는 GTypeFlags의 bitwise 조합이다. 이 열거체에서 정의하는 값에는 두 가지가 있다. G_TYPE_FLAG_ABSTRACT는 타입이 추상적임을 나타낸다. 따라서 추상적 타입의 인스턴스는 생성하지 못하도록 막을 것이다. 나머지 플래그 G_TYPE_FLAG_VALUE_ABSTRACT는 값 테이블과 같은 추상적 값의 타입을 나타내지만 g_value_init()와 함께 사용할 수 없다. 함수는 주어진 매개변수에 대해 새로운 GType을 리턴한다.
새로운 GType을 준비하는 과정에서 마지막 단계는 my_ip_address_get_type()에서 새 값을 리턴하는 것인데, 새 값이 단순히 등록되었든 아니면 정적인 값에 의해 저장되었든 상관없이 리턴한다. 해당 함수는 먼저 새로운 타입을 등록한 후 유일한 GType 값을 검색하는 데에 사용된다. 리턴된 값은 MyIPAddress로부터 새 위젯을 상속할 때나 새로운 MyIPAddress 위젯을 생성할 때 등 많은 장소에서 사용이 가능하다.
위젯 클래스 초기화하기
소스 파일의 다음 함수는 클래스 초기화 함수의 구현으로 (GClassInitFunc), my_ip_address_class_init()에 의해 제공된다. 이 함수는 타입을 등록 시 명시된 선택적 gpointer 데이터 매개변수와 MyIPAddressClass 객체를 수락한다. 리스팅 11-7에서는 두 번째 매개변수가 무시되었는데, 새로운 GType을 정의할 때 사용자 데이터 매개변수가 NULL로 정의되었기 때문이다.
리스팅 11-7. MyIPAddressClass 초기화하기
static void
my_ip_address_class_init (MyIPAddressClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
/* Override the standard functions for setting and retrieving properties. */
gobject_class->set_property = my_ip_address_set_property;
gobject_class->get_property = my_ip_address_get_property;
/* Add MyIPAddressPrivate as a private data class of MyIPAddressClass. */
g_type_class_add_private (klass, sizeof (MyIPAddressPrivate));
/* Register the ip-changed signal, which will be emitted when the ip changes. */
my_ip_address_signals[CHANGED_SIGNAL] =
g_signal_new ("ip-changed", G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (MyIPAddressClass, ip_changed),
NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
/* Register four GObject properties, one for each ip address number. */
g_object_class_install_property (gobject_class, PROP_IP1,
g_param_spec_int ("ip-number-1",
"IP Address Number 1",
"The first IP address number",
0, 255, 0,
G_PARAM_READWRITE));
g_object_class_install_property (gobject_class, PROP_IP2,
g_param_spec_int ("ip-number-2",
"IP Address Number 2",
"The second IP address number",
0, 255, 0,
G_PARAM_READWRITE));
g_object_class_install_property (gobject_class, PROP_IP3,
g_param_spec_int ("ip-number-3",
"IP Address Number 3",
"The third IP address number",
0, 255, 0,
G_PARAM_READWRITE));
g_object_class_install_property (gobject_class, PROP_IP4,
g_param_spec_int ("ip-number-4",
"IP Address Number 1",
"The fourth IP address number",
0, 255, 0,
G_PARAM_READWRITE));
}
클래스 초기화 함수에서 가장 먼저 해야 할 일은 위젯 클래스를 상속받을 GObjectClass에 필요한 함수를 모두 오버라이드하는 일이다. 이번 예제에서는 set_property()와 get_property()의 기본 구현을 오버라이드해야 했다. 이 함수들은 프로그래머가 각각 g_object_set()과 g_object_get()을 호출하면 호출되었다. 새로운 객체에 설치된 프로퍼티가 하나라도 있다면 이러한 함수들은 항상 오버라이드해야 한다.
GObjectClass에는 생성자(constructor), notify 시그널 콜백, 최종화 함수를 포함해 다른 많은 함수들이 제공된다. 오버라이드가 가능한 함수의 전체 리스트는 GObject API 문서에서 찾을 수 있다.
다음으로 MyIPAddressPrivate의 인스턴스는 g_type_class_add_private()을 이용해 위젯 클래스로 연결된다. 이 구조체는 위젯 프로퍼티에 대한 값을 보유할 것이다.
void g_type_class_add_private (gpointer klass,
gsize private_size);
이 함수의 첫 번째 매개변수는 private 클래스와 연관될 위젯 클래스다. 그 다음에는 private 구조체의 크기가 따라오는데, sizeof()를 이용해 얻을 수 있다. GObject는 이러한 방식으로 private 데이터를 구현함으로써 C 프로그래밍 언어에서 허용하는 범위까지 데이터 숨김을 제공한다.
시그널 설치하기
필요한 가상 함수를 오버라이드했다면 그 다음은 위젯 클래스 초기화 함수에서 g_signal_new()를 이용해 자신의 객체에서 필요로 하는 시그널을 준비해야 한다. 이는 매우 길고 복잡한 함수이므로 매개변수를 한 번에 하나씩 살펴보도록 하겠다.
guint g_signal_new (const gchar *signal_name,
GType class_type,
GSignalFlags signal_flags,
guint class_offset,
GSignalAccumulator accumulator,
gpointer accumulator_data,
GSignalCMarshaller c_marshaller,
GType return_type,
guint n_parameters,
...);
g_signal_new()의 첫 번째 매개변수는 자신이 생성 중인 새로운 시그널의 이름이다. 이번 예제에서는 ip-address 시그널을 추가하고 있다. 이 이름은 콜백 함수로 시그널을 연결할 때 프로그래머가 g_signal_connect() 함수군(family of functions)에서 사용할 것이다. 프로그래머가 이름만으로 목적을 알아차리도록 가능한 한 설명적인 이름으로 만드는 것이 중요하다.
g_signal_new()의 다음 매개변수는 시그널을 포함하게 될 클래스의 GType을 제공한다. 이러한 타입은 인스턴스에서 G_TYPE_FROM_CLASS() 또는 G_OBJECT_CLASS_TYPE()을 호출하여 검색 가능하다. 그 다음에는 GSignalFlags 열거값에 정의된 시그널 플래그의 bitwise 리스트가 따라오는데, 그 정의는 다음과 같다.
- G_SIGNAL_RUN_FIRST: 첫 번째 발생단계(emission stage) 중에 이 시그널에 대한 핸들러를 호출한다. 이 플래그 집합과 함께 다른 객체 시그널이 실행 중일 때 실행될 것이다.
- G_SIGNAL_RUN_LAST: 세 번째 발생단계(emission stage) 중에 이 시그널에 대한 핸들러를 호출한다. 이 플래그 집합과 함께 다른 객체 시그널이 실행 중일 때 실행될 것이다.
- G_SIGNAL_RUN_CLEANUP: 마지막 발생단계(emission stage) 중에 이 시그널에 대한 핸들러를 호출한다. 이 플래그 집합과 함께 다른 객체 시그널이 실행 중일 때 실행될 것이다.
- G_SIGNAL_NO_RECURSE: 해당 객체에서 시그널이 이미 발생한 경우 재귀적 호출이 금지될 것이다. 대신 첫 번째 발생이 재시작될 뿐이다.
- G_SIGNAL_DETAILED: 연결과 시그널의 발생 시 시그널명에 추가된 ::detaildescriptor 의 지원을 추가한다.
- G_SIGNAL_ACTION: 이것을 설정 시 객체에 대한 발생전, 발생후 조정을 실행하지 않고도 g_signal_emit()와 그 friends 를 이용해 해당 시그널을 발생시킬 수 있다. 이 객체를 이용하는 코드가 시그널을 발생하도록 허용하기 위함이다.
- G_SIGNAL_NO_HOOKS: 이 시그널에 대한 발생 훅(emission hook)을 지원하지 않는다.
g_signal_new()의 다음 매개변수는 시그널 프로토타입의 클래스에서 구조체 오프셋(structure offset)에 해당한다. 가령 MyIPAddressClass에서 ip_changed()의 오프셋을 얻기 위해 G_STRUCT_OFFSET()이 사용되었다. 이 함수는 아래 코드 조각에서 볼 수 있듯이 GLib에 의해 정의되는데, 아래 코드는 struct_type 내에서 member의 오프셋을 단순히 리턴하는 함수를 설명한다. 이는 g_signal_new()를 이용해 콜백 함수 프로토타입을 찾도록 해준다.
#define G_STRUCT_OFFSET(struct_type, member) \
((glong) ((guint8*) &((struct_type*) 0)->member))
다음으로 축적(accumulation)에 사용될 GSignalAccumulator 타입의 선택적 함수를 명시한 다음 그 함수로 전달될 데이터를 명시할 수 있다. 대부분의 경우는 두 가지 매개변수 모두 NULL로 설정될 것이다.
그 다음 매개변수 GSignalCMarshaller는 closure marshal 함수라고 부르는데, 매개변수 집합체를 C가 지원하는 콜백 함수로 번역하는 데에 사용된다. GObject는 표준 명명 규칙을 따르는 closure marshal 함수를 다수 제공한다. 대부분은 개발자가 별도로 생성할 필요가 없다.
가장 기본적 타입의 closure marshal 함수는 g_cclosure_marshal_VOID__VOID()다. 함수에 두 개의 밑줄 문자가 연속으로 포함되어 있는 표기법을 주목하라! 첫 번째 VOID는 GObject에게 콜백 함수의 리턴 타입이 void임을 알린다. 두 번째 VOID는 콜백 함수로 전송된 사용자 데이터와 인스턴스 외에 추가 매개변수가 없다고 말한다. 이러한 시그널 타입의 함수 프로토타입은 다음과 같다.
void (*callback) (gpointer instance, gpointer data);
또 다른 예로 g_cclosure_marshal_VOID__BOOLEAN()가 있다. 해당 시그널 타입에 해당하는 콜백 함수 프로토타입은 다음과 같다. 이는 void를 리턴하고, 객체 인스턴스와 사용자 데이터 사이에 위치한 추가 gboolean 매개변수를 갖고 있다.
void (*callback) (gpointer instance, gboolean arg, gpointer data);
위의 두 가지 closure marshal 함수 외에도 다른 기본적인 타입을 리턴하는 함수들도 많다. GObject 또한 void가 아닌 리턴값을 몇 가지 제공한다. 가령 g_cclosure_marshal_STRING_OBJECT_POINTER()는 C 문자열을 리턴하고 두 개의 매개변수, 즉 GObject와 포인터를 수락한다. GLib 2.12에서 이용 가능한 closure marshal 함수의 전체 리스트는 다음과 같다.
- g_cclosure_marshal_VOID__*(): 이 함수들은 어떤 것도 리턴하지 않고 BOOLEAN, CHAR, UCHAR, INT, UINT, LONG, ULONG, ENUM, FLAGS, FLOAT, DOUBLE, STRING, PARAM, BOXED, POINTER, OBJECT, UINT_POINTER 중에 하나의 추가 매개변수만 수락한다. VOID의 값은 콜백 함수로 하여금 두 개의 기본 매개변수만 갖도록 할 것이다.
- g_cclosure_marshal_STRING__OBJECT_POINTER(): 이 함수는 문자열을 리턴하고 포인터와 GObject의 추가 매개변수를 수락한다.
- g_cclosure_marshal_BOOLEAN__FLAGS(): 이 함수는 gboolean 값을 리턴하고 G_TYPE_FLAGS가 정의한 bitwise 필드를 수락한다.
g_signal_new()에서 다음 매개변수는 콜백 함수에 사용될 리턴 타입을 제공한다. 가령 MyIPAddress 예제에서 콜백 함수는 리턴값을 갖고 있지 않으므로 G_TYPE_NONE이라고 부른다. 기본적으로 등록되는 기본 타입의 전체 리스트는 표 11-2에 싣겠다.
GType | 정의 |
G_TYPE_BOOLEAN | TRUE 또는 FALSE를 보유하는 표준 Boolean 타입 |
G_TYPE_BOXED | 박스(boxed) 또는 구조체 타입을 나타내는 기본 타입 |
G_TYPE_CHAR G_TYPE_UCHAR |
표준 C char 타입에 대한 부호가 있는(signed) 버전과 부호가 없는(unsigned) 버전 |
G_TYPE_DOUBLE | 표준 C double 타입과 동일한 gdouble 변수 |
G_TYPE_ENUM | C enum 타입과 동일한 표준 열거 |
G_TYPE_FLAGS | Boolean 플래그를 보유하는 bitwise 필드 |
G_TYPE_FLOAT | 표준 C float 타입과 동일한 gfloat 변수 |
G_TYPE_INT G_TYPE_UINT |
표준 C int 타입에 대한 부호가 있는 버전과 부호가 없는 버전 |
G_TYPE_INT64 G_TYPE_UINT64 |
GLib의 64 비트 정수 구현에 대한 부호가 있는 버전과 부호가 없는 버전 |
G_TYPE_INTERFACE | 인터페이스를 상속할 수 있는 기본 타입 |
G_TYPE_INVALID | 일부 함수에 의해 오류 리턴값으로 사용되는 무효한(invalid) GType |
G_TYPE_LONG G_TYPE_ULONG |
표준 C long 타입에 대한 부호가 있는 버전과 부호가 없는 버전 |
G_TYPE_NONE | void와 동일하게 빈 타입 |
G_TYPE_OBJECT | GObject로 캐스팅되고 GObject에서 상속된 클래스를 나타내는 기본 타입 |
G_TYPE_PARAM | GParamSpec로부터 상속된 타입이라면 무엇이든 나타내는 기본 타입 |
G_TYPE_POINTER | void 포인터로 구현되는 타입이 정해지지 않은(untyped) 포인터 타입 |
G_TYPE_STRING | gchar 문자 배열에 대한 포인터로 저장된 NULL로 끝나는 C 문자열 |
표 11-2. 기본적인 GLib 타입 |
g_signal_new()에서 마지막 매개변수는 콜백 함수가 수락하는 매개변수의 개수인데, 인스턴스와 사용자 데이터에 이어 각 매개변수에 대한 타입 리스트는 제외한다. 우리 예제에는 별도의 타입이 추가되지 않았기 때문에 매개변수의 개수가 0으로 설정되었다. GtkEntry 위젯의 populate-popup 시그널에 대한 선언을 아래에서 살펴보자.
g_signal_new ("populate_popup",
G_OBJECT_CLASS_TYPE (gobject_class), /* or G_TYPE_FROM_CLASS() */
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GtkEntryClass, populate_popup),
NULL, NULL,
_gtk_marshal_VOID__OBJECT, /* defined in gtkmarshal.h */
G_TYPE_NONE, 1,
GTK_TYPE_MENU);
위의 시그널 선언에서는 하나의 추가 매개변수가 콜백 함수로 전송되었는데, 이는 GtkMenu로 캐스팅되었다. GtkMenu 타입은 GTK_TYPE_MENU에서 정의된다. 이는 g_cclosure_marshal_VOID__OBJECT()가 정의한 일반 GObject 대신에 사용할 수 있는 좀 더 구체적인 매개변수 캐스트 타입을 제공한다.
프로퍼티 설치하기
이번 예제의 클래스 초기화 함수에서 마지막으로 할 일은 필요한 프로퍼티를 설치하는 일이다. MyIPAddress 위젯에는 네 개의 프로퍼티가 설치되어 있는데, 모두 정수에 해당한다.
프로퍼티는 g_object_class_install_property()를 이용해 GObjectClass에 설치된다. 이 함수에서 첫 두 개의 매개변수는 자신의 새로운 위젯 클래스와 프로퍼티 식별자에 해당하는 GObjectClass를 수락한다. 식별자는 부호가 없는 유일한 정수로서 특정 프로퍼티를 참조한다. 이러한 식별자들은 MyIPAddress에서와 마찬가지로 일반적으로 열거에 정의되어 식별자가 객체에 유일하도록 보장한다.
void g_object_class_install_property (GObjectClass *object_class,
guint property_id,
GParamSpec *pspec);
g_object_class_install_property()의 마지막 매개변수는 GParamSpec 객체로서, 프로퍼티가 보유하는 변수의 타입, 이름, 다양한 특성에 대한 정보를 포함한다. GParamSpec 객체를 설정하도록 많은 함수가 제공된다.
IPAddress 예제에서는 g_param_spec_int()를 이용해 G_TYPE_INT 타입의 프로퍼티에 대한 새로운 GParamSpecInt 구현을 준비하였다. 이 함수에서 첫 세 개의 매개변수는 프로퍼티명, 프로퍼티에 대한 짧은 별칭(short nickname), 프로퍼티에 대한 설명을 각각 나타낸다. 프로퍼티명은 g_object_set()과 g_object_get()을 호출하여 접근할 것이다.
GParamSpec* g_param_spec_int (const gchar *name,
const gchar *nick,
const gchar *blurb,
gint minimum,
gint maximum,
gint default_value,
GParamFlags flags);
그 다음 세 개의 매개변수는 프로퍼티에 가능한 최소값, 최대값, 기본값을 정의한다. 이러한 값들은 프로퍼티 범위를 비롯해 초기 상태를 정의하는 데에 사용된다. 마지막 매개변수는 프로퍼티에 적용 가능한 아래의 GParamFlags 열거로부터 플래그를 정의하도록 해준다.
- G_PARAM_READABLE: 매개변수의 값을 읽는 것이 가능하다.
- G_PARAM_WRITABLE: 매개변수에 대해 새 값을 쓰는 것이 가능하다.
- G_PARAM_CONSTRUCT: 객체의 생성이 완료되면(constructed) 매개변수가 설정될 것이다.
- G_PARAM_CONSTRUCT_ONLY: 객체의 생성이 완료되었을 때에만 해당 매개변수가 설정될 것이다.
- G_PARAM_LAX_VALIDATION: g_param_value_convert()를 이용해 매개변수를 변환하면 엄격한 검증(strict validation)이 필요하지 않을 것이다.
- G_PARAM_STATIC_NAME: 매개변수가 존재하는 동안 매개변수명은 절대 수정되는 일이 없고 유효하게 남을 것이다.
- G_PARAM_STATIC_NICK: 매개변수가 존재하는 동안 매개변수의 별칭은 절대 수정되는 일이 없고 유효하게 남을 것이다.
- G_PARAM_STATIC_BLURB: 매개변수가 존재하는 동안 매개변수 설명은 절대 수정되는 일이 없고 유효하게 남을 것이다.
G_PARAM_READWRITE라는 추가 플래그도 있는데, 이는 (G_PARAM_READABLE | G_PARAM_WRITABLE)에 대한 bitwise alias로 정의된다. 이는 GParamFlags의 열거 값 대신 매크로로서 포함된다.
GLib에서 제공되는 모든 기본적인 데이터 타입에 이용 가능한 GParamSpec 구조체들이 존재한다. 다른 프로퍼티 타입을 생성하기 위한 함수 프로토타입의 전체 리스트는 GObject API 문서를 참조하도록 한다.
매개변수와 값 정의
여러 가지의 기본 타입에 대해 다수의 GParamSpec 및 GValue 함수가 정의되어 있다. 아래 리스트는 이러한 함수 각각에 대한 설명을 제공한다. 아래 각 함수에서 별표 문자는 함수의 경우에 따라 boolean, char, uchar, int, uint, long, ulong, int64, uint64, float, double, enum, flags, string, param, boxed, pointer, object, gtype 중 하나로 대체가 가능하다.
- G_IS_PARAM_SPEC_*(): 주어진 객체가 주어진 타입에 유효한 매개변수 명세(parameter specification) 객체일 경우 TRUE를 리턴한다.
- G_PARAM_SPEC_*(): GParamSpecobject 를 특정 매개변수 명세 타입으로 캐스팅한다.
- G_VALUE_HOLDS_*(): 주어진 GValu가 함수에서 정의한 타입을 보유할 수 있는 경우 TRUE를 리턴한다.
- G_TYPE_PARAM_*(): 주어진 매개변수 명세 타입에 대한 GType을 리턴한다.
- g_param_spec_*(): 주어진 타입의 새로운 매개변수 명세를 생성한다. 이 함수는 주로 GObject에 대한 새로운 프로퍼티를 정의할 때 사용된다. 모든 함수는 프로퍼티명, 별칭, 간략한 설명, GParamFlags의 bitwise 리스트, 주어진 타입에 관련된 매개변수를 수락한다. 리턴값은 새로운 GParamSpec 객체가 된다.
- g_value_set_*(): GValue 객체가 저장한 값을 주어진 변수로 설정한다. 새로운 값은 함수와 동일한 타입이어야 한다.
- g_value_get_*(): 이미 주어진 타입으로 캐스팅된 GValue 객체가 저장한 값을 검색한다.
앞의 함수 리스트에서 별표 문자를 데이터 타입으로 대체할 때는 함수의 대·소문자를 매치해야 한다. 예를 들어 G_IS_PARAM_SPEC_*()에서 별표는 모두 대문자로 된 데이터 타입으로 대체되어야 한다. 상세한 예제는 API 문서를 참조한다.
객체 프로퍼티를 설정하고 검색하기
클래스 초기화 함수에서는 기본 함수 set_property()와 get_property()가 GObjectClass에서 오버라이드되었다. 새로운 GObject에 하나 또는 이상의 프로퍼티가 있다면 이 두 개의 함수는 오버라이드되어선 안 된다. 리스팅 11-8에서는 g_object_set()으로 전송된 프로퍼티마다 호출될 함수의 구현을 소개하겠다.
리스팅 11-8. 객체 프로퍼티 설정하기
static void
my_ip_address_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
MyIPAddress *ipaddress = MY_IP_ADDRESS (object);
gint address[4] = { -1, -1, -1, -1 };
switch (prop_id)
{
case PROP_IP1:
address[0] = g_value_get_int (value);
my_ip_address_set_address (ipaddress, address);
break;
case PROP_IP2:
address[1] = g_value_get_int (value);
my_ip_address_set_address (ipaddress, address);
break;
case PROP_IP3:
address[2] = g_value_get_int (value);
my_ip_address_set_address (ipaddress, address);
break;
case PROP_IP4:
address[3] = g_value_get_int (value);
my_ip_address_set_address (ipaddress, address);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
프로퍼티가 도착하면 어떤 객체 타입이든 저장할 수 있는 일반 컨테이너 GValue 객체로 저장된다. GValue가 저장한 정수값을 검색 시에는 g_value_get_int() 함수가 사용된다. 모든 기본 데이터 타입과 앞 절에서 정의한 GValue 객체 간 변환에 이용할 수 있는 함수들도 있다.
다음 단계는 프로퍼티가 유효할 경우 그것의 새로운 값을 저장하는 단계다. 프로퍼티 식별자는 prop_id에 저장되는데, 이를 이용해 설치된 프로퍼티와 비교하여 수정 중인 프로퍼티 식별자를 찾을 수 있다. 변경내용을 적용하는 데에는 my_ip_address_set_address() 함수가 사용되었다. 이 함수의 구현은 이번 절의 뒷부분에 소개하겠다.
이 함수가 제공하는 기능은 아래 코드 조각에 제시한 방법을 이용해도 구현이 가능하다. 하지만 위젯의 프로퍼티가 모두 동일한 타입이고 배열에 저장되는 경우가 극히 드물기 때문에 확장된 형태로 제시함을 명심한다.
address[prop_id-1] = g_value_get_int (value);
my_ip_address_set_address (ipaddress, address);
이번 장의 MyIPAddress 예제는 가장 간단한 예제에 속하기 때문에 상당히 크게 확장이 가능하다. 애플리케이션에서 이 위젯을 사용하기 위해 구현하는 중이라면 프로퍼티와 시그널 뿐만 아니라 추가 기능도 제공하길 원할 것이다. 이 점을 유념하면서 계속 살펴보도록 한다.
객체 클래스의 기본 함수 get_property()가 오버라이드되었다. 따라서 MyIPAddress의 프로퍼티에서 g_object_set()이 호출되면 리스팅 11-9에서와 같이 my_ip_address_get_property()가 호출될 것이다.
리스팅 11-9. 객체 프로퍼티 검색하기
static void
my_ip_address_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
MyIPAddress *ipaddress = MY_IP_ADDRESS (object);
MyIPAddressPrivate *priv = MY_IP_ADDRESS_GET_PRIVATE (ipaddress);
switch (prop_id)
{
case PROP_IP1:
g_value_set_int (value, priv->address[0]);
break;
case PROP_IP2:
g_value_set_int (value, priv->address[1]);
break;
case PROP_IP3:
g_value_set_int (value, priv->address[2]);
break;
case PROP_IP4:
g_value_set_int (value, priv->address[3]);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
my_ip_address_get_property() 함수는 MyIPAddressPrivate 구조체에서 적절한 프로퍼티를 취해 GValue로 변환한다. 이후 새 값은 사용자의 변수로 적용되어 올바른 변수로 캐스팅된다. Private 구조체는 소스 파일의 맨 위에 정의된 MY_IP_ADDRESS_GET_PRIVATE() 함수를 이용해 검색된다.
위젯 인스턴스화하기
또 구현해야 하는 초기화 함수로 my_ip_address_init()가 있는데, 이는 새로운 MyIPAddress 위젯이 생성될 때마다 호출된다. 이러한 함수는 객체가 인스턴스화될 때마다 호출되는 것이 아니라 객체 클래스를 설정할 때만 호출되는 클래스 초기화 함수와는 차이가 있다. 리스팅 11-10에 표시된 인스턴스 초기화 함수는 초기 IP 주소값을 0으로 설정하고, 초기 렌더링을 실행하며, 필요한 시그널을 연결하는 일을 수행한다.
리스팅 11-10. MyIPAddress 객체 인스턴스화하기
static void
my_ip_address_init (MyIPAddress *ipaddress)
{
MyIPAddressPrivate *priv = MY_IP_ADDRESS_GET_PRIVATE (ipaddress);
PangoFontDescription *fd;
guint i;
for (i = 0; i < 4; i++)
priv->address[i] = 0;
fd = pango_font_description_from_string ("Monospace");
gtk_widget_modify_font (GTK_WIDGET (ipaddress), fd);
my_ip_address_render (ipaddress);
pango_font_description_free (fd);
/* The key-press-event signal will be used to filter out certain keys. We will
* also monitor the cursor-position property so it can be moved correctly. */
g_signal_connect (G_OBJECT (ipaddress), "key-press-event",
G_CALLBACK (my_ip_address_key_pressed), NULL);
g_signal_connect (G_OBJECT (ipaddress), "notify::cursor-position",
G_CALLBACK (my_ip_address_move_cursor), NULL);
}
my_ip_address_init() 함수는 생성 및 캐스팅이 이미 완료된 MyIPAddress 객체를 수락한다. 여기서 개발자가 해야 할 일은 위젯이 개발자에게 리턴되어 사용자에게 표시되기 전에 위젯에서 실행되어야 하는 추가 처리를 실행하는 것이다.
이번 예제에서 함수는 먼저 네 개의 IP 주소값을 0으로 초기화한다. 위젯의 글꼴은 Monospace로 설정된다. 크기는 명시되지 않으므로 사용자의 테마에 따른 크기가 허용됨을 주목한다. 이는 글꼴이 크게 설정된 사용자도 위젯의 내용을 읽을 수 있도록 보장하기 위함이다.
마지막으로 MyIPAddress 위젯은 두 개의 시그널로 연결된다. key-press-event 콜백 함수는 위젯이 반응하게 될 키를 필터링할 것이다. 이후 cursor-position이 변경되면 위치가 업데이트되어 텍스트가 입력될 곳을 제어할 수 있게 된다. MyIPAddress는 GtkEntry에서 상속되므로 GtkEntry의 모든 member, 프로퍼티, 시그널, 함수 등도 상속받는다는 사실을 명심한다. 뿐만 아니라 조상 클래스에 해당하는 GtkWidget, GtkObject, GObject에서도 모든 것을 상속받는다.
다음으로 위젯이 사용자와 상호작용하는 방식을 처리하도록 몇 가지 private 함수들이 구현된다. 리스팅 11-11은 my_ip_address_render()라는 함수를 소개한다. 이 함수는 IP 주소값에서 문자열을 빌드하여 GtkEntry 위젯으로 추가한다. 이는 GtkEntry 위젯으로 작성하는 유일한 함수다.
리스팅 11-11. MyIPAddress 위젯 렌더링하기
/* Render the current content of the IP address in the GtkEntry widget. */
static void
my_ip_address_render (MyIPAddress *ipaddress)
{
MyIPAddressPrivate *priv = MY_IP_ADDRESS_GET_PRIVATE (ipaddress);
GString *text;
guint i;
/* Create a string that displays the IP address content, adding spaces if a
* number cannot fill three characters. */
text = g_string_new (NULL);
for (i = 0; i < 4; i++)
{
gchar *temp = g_strdup_printf ("%3i.", priv->address[i]);
text = g_string_append (text, temp);
g_free (temp);
}
/* Remove the trailing decimal place and add the string to the GtkEntry. */
text = g_string_truncate (text, 15);
gtk_entry_set_text (GTK_ENTRY (ipaddress), text->str);
g_string_free (text, TRUE);
}
이 함수는 현재 MyIPAddressPrivate의 인스턴스에 저장된 네 개의 정수와 세 개의 마침표(period)를 이용해 15개 문자로 된 IP 주소를 빌드하기 위해 GString을 이용한다. 이 문자열은 GtkEntry 위젯에서 사용자에게 표시된다. 정수가 세 개의 공간을 차지하지 않을 경우 하나 또는 두 개의 공백(space) 문자로 패딩되어 IP 주소는 항상 15개 문자 너비를 가질 것이다. 너비가 보장되어야만 커서가 위치해야 할 정확한 장소를 언제든지 알 수 있다.
MyIPAddress 위젯은 커서가 네 개의 위치 중 하나에 강제로 위치하도록 빌드된다. 각 숫자는 우측으로 정렬되고, 필요 시 좌측에 공백이 패딩된다. 이 때문에 커서는 네 개 중 하나의 숫자 우측에 강제로 위치된다. 이는 리스팅 11-12에 표시된 notify::cursor-position 콜백 함수에서 실행된다.
리스팅 11-12. MyIPAddress에 대한 콜백 함수
/* Force the cursor to always be at the end of one of the four numbers. */
static void
my_ip_address_move_cursor (GObject *entry,
GParamSpec *spec)
{
gint cursor = gtk_editable_get_position (GTK_EDITABLE (entry));
if (cursor <= 3)
gtk_editable_set_position (GTK_EDITABLE (entry), 3);
else if (cursor <= 7)
gtk_editable_set_position (GTK_EDITABLE (entry), 7);
else if (cursor <= 11)
gtk_editable_set_position (GTK_EDITABLE (entry), 11);
else
gtk_editable_set_position (GTK_EDITABLE (entry), 15);
}
/* Handle key presses of numbers, tabs, backspaces and returns. */
static gboolean
my_ip_address_key_pressed (GtkEntry *entry,
GdkEventKey *event)
{
MyIPAddressPrivate *priv = MY_IP_ADDRESS_GET_PRIVATE (entry);
guint k = event->keyval;
gint cursor, value;
/* If the key is an integer, append the new number to the address. This is only
* done if the resulting number will be less than 255. */
if ((k >= GDK_0 && k <= GDK_9) || (k >= GDK_KP_0 && k <= GDK_KP_9))
{
cursor = floor (gtk_editable_get_position (GTK_EDITABLE (entry)) / 4);
value = g_ascii_digit_value (event->string[0]);
if ((priv->address[cursor] == 25) && (value > 5))
return TRUE;
if (priv->address[cursor] < 26)
{
priv->address[cursor] *= 10;
priv->address[cursor] += value;
my_ip_address_render (MY_IP_ADDRESS (entry));
gtk_editable_set_position (GTK_EDITABLE (entry), (4 * cursor) + 3);
g_signal_emit_by_name ((gpointer) entry, "ip-changed");
}
}
/* Move to the next number or wrap around to the first. */
else if (k == GDK_Tab)
{
cursor = (floor (gtk_editable_get_position (GTK_EDITABLE (entry)) / 4) + 1);
gtk_editable_set_position (GTK_EDITABLE (entry), (4 * (cursor % 4)) + 3);
}
/* Delete the last digit of the current number. This just divides the number by
* 10, relying on the fact that any remainder will be ignored. */
else if (k == GDK_BackSpace)
{
cursor = floor (gtk_editable_get_position (GTK_EDITABLE (entry)) / 4);
priv->address[cursor] /= 10;
my_ip_address_render (MY_IP_ADDRESS (entry));
gtk_editable_set_position (GTK_EDITABLE (entry), (4 * cursor) + 3);
g_signal_emit_by_name ((gpointer) entry, "ip-changed");
}
/* Activate the GtkEntry widget, which corresponds to the activate signal. */
else if ((k == GDK_Return) || (k == GDK_KP_Enter))
gtk_widget_activate (GTK_WIDGET (entry));
return TRUE;
}
리스팅 11-12에는 두 번째 함수 my_ip_address_key_pressed()도 포함되어 있는데, 이 함수는 key-press-event 시그널이 발생할 때 호출된다. 해당 함수는 특정 키만 처리하고 그 외에는 모두 무시한다. 가령, 숫자 키만 처리하고 문자와 기호는 모두 무시되는 경우를 예로 들 수 있겠다. 처리되는 키 집합을 한 번에 하나씩 살펴보도록 하겠다.
첫 번째 조건문(conditional)은 <gdk/gdkkeysyms.h>에 정의된 바와 같이 키보드의 상단이든 키패드든 키보드에서 누른 숫자를 처리한다. GDK_KP_#는 숫자 패드의 숫자 키에 해당하고 GDK_#는 키보드 상단에 위치한 숫자 키에 해당하는데, 둘 다 조건문에서 설명되어야 한다.
커서 위치를 0과 3 사이의 숫자로 변환하기 위해 floor() 함수가 사용되었는데, IP 주소값이 그 값으로 편집되어야 함을 나타낸다. 이벤트 문자열 또한 g_ascii_digit_value()를 이용해 정수로 변환된다.
이제 필요한 값이 모두 있으니 두 개의 조건문이 새 값의 유효성을 검사한다. 새로운 값이 255를 초과하지 않을 경우에 이 새로운 정수는 현재값의 뒤에 추가된다. 숫자가 범위 내에 있다면 현재 값이 스케일링되고(scaled), 새로운 정수는 뒤에 추가된다. 그 다음 새로운 IP 주소가 GtkEntry 위젯에서 렌더링되고, 커서 위치가 새로고침(refresh)된다. 마지막으로 g_signal_emit_by_name()을 이용해 IP 주소가 변경되었음을 사용자에게 알린다.
두 번째 조건문은 Tab 키를 처리하는데, 이 키를 누르면 네 개의 숫자를 각각 순환할 것이다. 위젯의 또 다른 구현에서는 위젯의 끝에 도달하면 탭 순으로 다음 위젯을 순환하도록 수정할 수도 있다.
다음으로 Backspace 키는 현재 값을 10으로 나눈다. 정수를 정수로 나누기 때문에 나머지 값은 무시되고, 마지막 자릿수는 없앤다(drop off). 이후 위젯이 렌더링되고 ip-changed 시그널이 발생한다.
마지막으로 Return 키와 Enter 키를 누르면 gtk_widget_activate()를 호출한다. 이는 사용자가 이러한 키를 이용해 MyIPAddress 위젯 내부로부터 창의 기본 위젯을 활성화하도록 해준다. 이번 절에서 다룬 키를 제외한 모든 키 누름은 무시된다.
Public MyIPAddress 함수 구현하기
위젯을 생성하기 위한 마지막 단계는 위젯의 헤더 파일에 선언된 public 함수들을 구현하는 것이다. 첫 번째 함수는 my_ip_address_new()로, 리스팅 11-13에서 새로운 MyIPAddress 위젯을 생성하는 데에 사용하였다.
리스팅 11-13. 새로운 MyIPAddress 위젯 생성하기
GtkWidget*
my_ip_address_new ()
{
return GTK_WIDGET (g_object_new (my_ip_address_get_type (), NULL));
}
이 함수는 g_object_new()가 리턴한 객체 GtkWidget로 캐스팅하는 작업만 제공함을 눈치챌 것이다. 이는 많은 위젯에서 편의 함수에 불과하므로 프로그래머는 GObject 인스턴스 자체를 생성할 필요가 없다. 초기화 함수로 매개변수를 수락하는 위젯을 사용 중이라면 이 곳에서 처리해야 할 것이다.
리스팅 11-14의 my_ip_address_get_address() 함수는 현재 위젯이 저장한 IP 주소의 문자열 표현을 리턴한다. 이렇게 리턴된 문자열을 모두 사용하고 나면 해제하는 일도 프로그래머의 몫인데, 문자열이 g_strdup_printf()를 이용해 생성되기 때문이다. 사용자는 이를 프로그램적으로 생성할 수도 있지만 대부분의 위젯은 자주 필요한 작업을 수행하는 수많은 편의 함수들을 제공하는 것이 보통이다.
리스팅 11-14. 현재 IP 주소 검색하기
gchar*
my_ip_address_get_address (MyIPAddress *ipaddress)
{
MyIPAddressPrivate *priv = MY_IP_ADDRESS_GET_PRIVATE (ipaddress);
return g_strdup_printf ("%d.%d.%d.%d", priv->address[0], priv->address[1],
priv->address[2], priv->address[3]);
}
마지막 함수 my_ip_address_set_address()는 프로그램의 변경내용을 IP 주소로 적용하는데, 이를 리스팅 11-15에 소개하겠다. 함수가 0보다 작거나 255보다 큰 숫자를 필터링하는 모습을 확인할 수 있을 것이다. 이를 통해 프로그래머는 IP 주소 숫자마다 새로운 값을 제공하지 않아도 된다. 다시 말해, 프로그래머가 하나의 값만 업데이트하면 되기 때문에 프로그램적으로 IP 주소를 업데이트하도록 하나의 함수만 제공하면 된다는 말이다.
리스팅 11-15. 새로운 IP 주소 설정하기
void
my_ip_address_set_address (MyIPAddress *ipaddress,
gint address[4])
{
MyIPAddressPrivate *priv = MY_IP_ADDRESS_GET_PRIVATE (ipaddress);
guint i;
for (i = 0; i < 4; i++)
{
if (address[i] >= 0 && address[i] <= 255)
{
priv->address[i] = address[i];
}
}
my_ip_address_render (ipaddress);
g_signal_emit_by_name ((gpointer) ipaddress, "ip-changed");
}
위젯 테스트하기
이번 예제에서 마지막으로 해야 할 일은 위젯이 작동하는지 검사하는 일이다. 리스팅 11-16에 실린 코드는 새로운 MyIPAddress 위젯이 있는 창을 하나 생성한다. 초기 IP 주소로 1.20.35.255가 추가되고, ip-changed 시그널이 IP 주소의 현재 상태를 출력하는 콜백 함수로 연결된다.
리스팅 11-16. MyIPAddress 위젯 테스트하기 (ipaddresstest.c)
#include <gtk/gtk.h>
#include "myipaddress.h"
static void ip_address_changed (MyIPAddress*);
int main (int argc,
char *argv[])
{
GtkWidget *window, *ipaddress;
gint address[4] = { 1, 20, 35, 255 };
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (window), "MyIPAddress");
gtk_container_set_border_width (GTK_CONTAINER (window), 10);
g_signal_connect (G_OBJECT (window), "destroy",
G_CALLBACK (gtk_main_quit), NULL);
ipaddress = my_ip_address_new ();
my_ip_address_set_address (MY_IP_ADDRESS (ipaddress), address);
g_signal_connect (G_OBJECT (ipaddress), "ip-changed",
G_CALLBACK (ip_address_changed), NULL);
gtk_container_add (GTK_CONTAINER (window), ipaddress);
gtk_widget_show_all (window);
gtk_main ();
return 0;
}
/* When the IP address is changed, print the new value to the screen. */
static void
ip_address_changed (MyIPAddress *ipaddress)
{
gchar *address = my_ip_address_get_address (ipaddress);
g_print ("%s\n", address);
g_free (address);
}
위의 MyIPAddress 위젯은 새로운 위젯을 생성하는 가장 간단한 예제에 속한다. 따라서 실제 애플리케이션에서 사용하려면 상당 부분 확장되어야 한다. 예를 들어서 개발자는 사용자가 위젯을 오른쪽 마우스로 클릭했을 때 표시되는 팝업 메뉴를 맞춤설정하길 원할 것이다. 또 프로그래머가 커스텀 IP 주소 포맷을 정의하도록 허용하는 경우도 예로 들 수 있겠다. 이번 절에서 다룬 내용을 더 잘 이해하기 위해서는 다음 절로 넘어가기 전에 MyIPAddress 위젯을 확장해볼 것을 권한다.
처음부터 위젯 생성하기
이미 존재하는 위젯에서 새로운 위젯을 상속하는 방법은 학습했으니 이제 처음부터 위젯을 생성하는 방법을 배워볼 차례다. 이번 절에 소개된 코드의 대부분은 앞에서 소개한 코드와 비슷함을 눈치챌 것이다. 이는 상속된 위젯과 새로운 위젯 모두 GObject의 기본 타입이어서 약간의 수고는 더 들여야 하지만 구현 방식은 동일하기 때문이다.
이번 절에서는 MyMarquee라고 불리는 위젯의 구현 방법을 학습할 것이다. 이 위젯은 위젯의 우측에서 좌측으로 메시지를 반복하여 스크롤한다. 이번 장에 실린 연습문제에서도 이 위젯을 확장할 것이기 때문에 위젯을 잘 이해하는 편이 좋을 것이다.
MyMarquee 위젯의 스크린샷은 그림 11-2에서 확인할 수 있다. 여느 예제와 마찬가지로 위젯에 대한 소스 코드는 서적의 웹 사이트에서 다운로드 가능하다.
MyMarquee 헤더 파일 생성하기
새로운 위젯을 준비하기 위한 첫 번째 단계는 헤더 파일을 생성하는 것이다. 헤더 파일을 생성하면 위젯을 제어하는 데에 사용될 프로그램적 인터페이스를 정의할 수 있도록 해준다. 리스팅 11-17(원문에 11-7로 되어 있어 수정하였습니다)은 MyMarquee 위젯에 대한 전체 헤더 파일을 제공하는데, MyIPAddress 헤더 파일과 매우 유사할 것이다.
리스팅 11-17. MyMarquee 위젯 헤더 (mymarquee.h)
#ifndef __MY_MARQUEE_H__
#define __MY_MARQUEE_H__
#include <glib.h>
#include <gdk/gdk.h>
#include <gtk/gtkwidget.h>
G_BEGIN_DECLS
#define MY_MARQUEE_TYPE (my_marquee_get_type ())
#define MY_MARQUEE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
MY_MARQUEE_TYPE, MyMarquee))
#define MY_MARQUEE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), \
MY_MARQUEE_TYPE, MyMarqueeClass))
#define IS_MY_MARQUEE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
MY_MARQUEE_TYPE))
#define IS_MY_MARQUEE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), \
MY_MARQUEE_TYPE))
typedef struct _MyMarquee MyMarquee;
typedef struct _MyMarqueeClass MyMarqueeClass;
struct _MyMarquee
{
GtkWidget widget;
};
struct _MyMarqueeClass
{
GtkWidgetClass parent_class;
};
GType my_marquee_get_type (void) G_GNUC_CONST;
GtkWidget* my_marquee_new (void);
void my_marquee_set_message (MyMarquee *marquee, const gchar *message);
gchar* my_marquee_get_message
(MyMarquee *marquee);
void my_marquee_set_speed (MyMarquee *marquee, gint speed);
gint my_marquee_get_speed (MyMarquee *marquee);
void my_marquee_slide (MyMarquee *marquee);
G_END_DECLS
#endif /* __MY_MARQUEE_H__ */
MyMarquee는 새로운 위젯이기 때문에 GtkWidget에서 직접 상속될 것이다. 이는 MyMarquee가 GtkWidget 객체를 포함하고 MyMarqueeClass가 GtkWidgetClass 클래스를 포함한다는 사실로 확인할 수 있다. 이러한 member들 중 어떤 것도 포인터로 선언되어선 안 된다는 기억을 되살려보자! GtkWidget에서 위젯을 상속하면 이벤트 처리를 비롯해 모든 위젯에 공통되는 시그널과 프로퍼티를 모두 이용할 수 있다.
위젯에는 프로그래머가 설정 및 검색할 수 있는 프로퍼티가 두 개 있다. 사용자는 my_marquee_set_message()를 이용해 위젯이 스크롤할 메시지를 변경할 수 있다. 속도는 1과 50 사이의 정수에 해당한다. my_marquee_slide()가 호출될 때마다 그 정수에 해당하는 수의 픽셀만큼 메시지가 좌측으로 이동할 것이다.
MyMarquee 위젯 생성하기
헤더 파일이 생성되었으니 리스팅 11-18에서는 private 클래스의 선언, 프로퍼티 열거, 새로운 GType 생성과 같은 기본적인 초기화를 실행한다. 이 위젯에 연관된 새로운 시그널은 없으므로 시그널 열거와 시그널 식별자의 배열은 생략하였다.
리스팅 11-18. MyMarqueePrivate과 MyMarquee GType 정의하기 (mymarquee.c)
#include "mymarquee.h"
#define MARQUEE_MIN_WIDTH 300
#define MY_MARQUEE_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), \
MY_MARQUEE_TYPE, MyMarqueePrivate))
typedef struct _MyMarqueePrivate MyMarqueePrivate;
struct _MyMarqueePrivate
{
gchar *message;
gint speed;
gint current_x;
};
enum
{
PROP_0,
PROP_MESSAGE,
PROP_SPEED
};
/* Get a GType that corresponds to MyMarquee. The first time this function is
* called (on object instantiation), the type is registered. */
GType
my_marquee_get_type ()
{
static GType marquee_type = 0;
if (!marquee_type)
{
static const GTypeInfo marquee_info =
{
sizeof (MyMarqueeClass),
NULL,
NULL,
(GClassInitFunc) my_marquee_class_init,
NULL,
NULL,
sizeof (MyMarquee),
0,
(GInstanceInitFunc) my_marquee_init,
};
marquee_type = g_type_register_static (GTK_TYPE_WIDGET, "MyMarquee",
&marquee_info, 0);
}
return marquee_type;
}
리스팅 11-18은 MyMarquee 위젯의 구현 중 첫 번째 부분만 표시한다. 필요한 위젯 프로퍼티의 값을 보유하는 데에 사용될 MyMarqueePrivate 구조체를 생성함으로써 파일을 시작하고자 한다. 파일에는 표시되는 메시지, 스크롤 속도, 현재 메시지의 수평적 위치가 포함된다. 이 위치를 기준으로 메시지의 다음 위치가 계산되어 위젯의 크기조정을 쉽게 처리하도록 해준다.
MyMarquee는 GtkWidget에서 직접 상속되기 때문에 my_marquee_get_type()의 구현에서 볼 수 있듯이 위젯을 GTK_TYPE_WIDGET의 부모 클래스 타입으로 등록할 필요가 있겠다. my_marquee_get_type() 함수의 구현은 my_ip_address_get_type() 의 구현과 거의 동일하다.
리스팅 11-19에서는 MyMarquee 클래스와 인스턴스 초기화 함수를 표시하였다. my_marquee_class_init()에서는 GObjectClass 뿐만 아니라 GtkWidgetClass에서도 함수를 오버라이드함을 눈치챌 것이다.
리스팅 11-19. MyMarquee 클래스와 구조체 초기화하기
/* Initialize the MyMarqueeClass class by overriding standard functions,
* registering a private class and setting up signals and properties. */
static void
my_marquee_class_init (MyMarqueeClass *klass)
{
GObjectClass *gobject_class;
GtkWidgetClass *widget_class;
gobject_class = (GObjectClass*) klass;
widget_class = (GtkWidgetClass*) klass;
/* Override the standard functions for setting and retrieving properties. */
gobject_class->set_property = my_marquee_set_property;
gobject_class->get_property = my_marquee_get_property;
/* Override the standard functions for realize, expose, and size changes. */
widget_class->realize = my_marquee_realize;
widget_class->expose_event = my_marquee_expose;
widget_class->size_request = my_marquee_size_request;
widget_class->size_allocate = my_marquee_size_allocate;
/* Add MyMarqueePrivate as a private data class of MyMarqueeClass. */
g_type_class_add_private (klass, sizeof (MyMarqueePrivate));
/* Register four GObject properties, the message and the speed. */
g_object_class_install_property (gobject_class, PROP_MESSAGE,
g_param_spec_string ("message",
"Marquee Message",
"The message to scroll",
"",
G_PARAM_READWRITE));
g_object_class_install_property (gobject_class, PROP_SPEED,
g_param_spec_int ("speed",
"Speed of the Marquee",
"The percentage of movement every second",
1, 50, 25,
G_PARAM_READWRITE));
}
/* Initialize the actual MyMarquee widget. This function is used to set up
* the initial view of the widget and set necessary properties. */
static void
my_marquee_init (MyMarquee *marquee)
{
MyMarqueePrivate *priv = MY_MARQUEE_GET_PRIVATE (marquee);
priv->current_x = MARQUEE_MIN_WIDTH;
priv->speed = 25;
}
다음 단계는 GTypeInfo 객체에 의해 참조되는 클래스 및 인스턴스 초기화 함수를 구현하는 일이다. 이 예제에서는 부모 GObjectClass에서 함수를 오버라이드할 뿐만 아니라 GtkWidgetClass에서도 몇 가지 함수를 오버라이드해야 한다. 여기에는 위젯의 실현(realizing) 및 노출(exposing)을 비롯해 크기 요청과 할당(allocation)을 위한 호출의 오버라이드도 포함된다.
GtkWidgetClass에서 함수를 오버라이드할 때는 위젯에 결정적인 업무를 실행하므로 매우 주의를 기울여야 한다. 필요한 함수를 모두 실행하지 않으면 위젯을 사용할 수 없도록(unusable) 렌더링할 수도 있다. 이를 시도할 때는 다른 GTK+ 위젯들이 오버라이드된 함수를 어떻게 구현하는지 살펴볼 것을 권한다. 오버라이드가 가능한 함수의 전체 리스트는 <gtk/gtkwidget.h>에서 GtkWidgetClass 구조체를 참조한다.
MyMarqueePrivate 구조체는 g_type_class_add_private()을 이용해 클래스 초기화 함수에서 MyMarqueeClass로 추가되었다. 객체는 MyMarqueeClass 구조체의 member로 저장되지 않기 때문에 인스턴스 초기화 함수에서 볼 수 있듯이 MY_MARQUEE_GET_PRIVATE()의 정의를 이용해 MyMarqueePrivate 객체를 검색할 필요가 있겠다.
my_marque_init()에서 메시지의 현재 위치는 위젯의 우측 아래에 표시된다. my_marquee_slide()가 프로그램적으로 호출되면 메시지는 기본적으로 25 픽셀만큼 좌측으로 스크롤된다.
오버라이드된 set_property()와 get_property() 함수의 구현은 앞의 예제와 비슷하다. 이러한 함수들을 리스팅 11-20에 실었는데, 이들은 사용자가 위젯의 message와 speed 프로퍼티를 설정하고 검색하도록 해준다.
리스팅 11-20. MyMarquee 프로퍼티를 설정하고 검색하기
/* This function is called when the programmer gives a new value for a widget
* property with g_object_set(). */
static void
my_marquee_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
MyMarquee *marquee = MY_MARQUEE (object);
switch (prop_id)
{
case PROP_MESSAGE:
my_marquee_set_message (marquee, g_value_get_string (value));
break;
case PROP_SPEED:
my_marquee_set_speed (marquee, g_value_get_int (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
/* This function is called when the programmer requests the value of a widget
* property with g_object_get(). */
static void
my_marquee_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
MyMarquee *marquee = MY_MARQUEE (object);
MyMarqueePrivate *priv = MY_MARQUEE_GET_PRIVATE (marquee);
switch (prop_id)
{
case PROP_MESSAGE:
g_value_set_string (value, priv->message);
break;
case PROP_SPEED:
g_value_set_int (value, priv->speed);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
리스팅 11-21에는 my_marquee_new()의 구현을 실었다. 이 함수는 프로그래머가 새로운 MyMarquee 위젯을 생성할 때 호출할 수 있다. 단순한 편의 함수이므로 g_object_new()를 직접 호출하지 않아도 된다.
리스팅 11-21. 새로운 MyMarquee 위젯 생성하기
GtkWidget*
my_marquee_new ()
{
return GTK_WIDGET (g_object_new (my_marquee_get_type (), NULL));
}
위젯 실현하기
위젯의 구현이 MyIPAddress와 다른 곳은 바로 오버라이드된 GtkWidgetClass 함수들이다. 이 함수들 중 처음으로 소개할 함수는 리스팅 11-22에 소개된 my_marquee_realize()다. 해당 함수는 MyMarquee 인스턴스가 처음으로 실현될 때 호출된다.
리스팅 11-22. MyMarquee 위젯 실현하기
static void
my_marquee_realize (GtkWidget *widget)
{
MyMarquee *marquee;
GdkWindowAttr attributes;
gint attr_mask;
g_return_if_fail (widget != NULL);
g_return_if_fail (IS_MY_MARQUEE (widget));
/* Set the GTK_REALIZED flag so it is marked as realized. */
GTK_WIDGET_SET_FLAGS (widget, GTK_REALIZED);
marquee = MY_MARQUEE (widget);
/* Create a new GtkWindowAttr object that will hold info about the GdkWindow. */
attributes.x = widget->allocation.x;
attributes.y = widget->allocation.y;
attributes.width = widget->allocation.width;
attributes.height = widget->allocation.height;
attributes.wclass = GDK_INPUT_OUTPUT;
attributes.window_type = GDK_WINDOW_CHILD;
attributes.event_mask = gtk_widget_get_events (widget);
attributes.event_mask |= (GDK_EXPOSURE_MASK);
attributes.visual = gtk_widget_get_visual (widget);
attributes.colormap = gtk_widget_get_colormap (widget);
/* Create a new GdkWindow for the widget. */
attr_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL | GDK_WA_COLORMAP;
widget->window = gdk_window_new (widget->parent->window, &attributes, attr_mask);
gdk_window_set_user_data (widget->window, marquee);
/* Attach a style to the GdkWindow and draw a background color. */
widget->style = gtk_style_attach (widget->style, widget->window);
gtk_style_set_background (widget->style, widget->window, GTK_STATE_NORMAL);
gdk_window_show (widget->window);
}
my_marquee_realize()는 가장 먼저 위젯이 NULL이 아닌지 확인하고, 위젯이 MyMarquee 위젯인지 확인해야 한다. 두 테스트 중 하나라도 FALSE를 리턴할 경우 gtk_return_if_fail() 함수를 이용해 함수로부터 리턴하도록 하였다. 이 테스트들은 항상 실행해야 하는데, 이를 어길 시 프로그램은 예기치 못한 반응을 보일 수 있다.
실현(realization) 함수를 사용하는 목적은 위젯의 인스턴스에 대해 GdkWindow를 준비하여 화면으로 렌더링이 가능하도록 만들기 위함이다. 이를 위해서는 먼저 새로운 GdkWindow의 원하는 프로퍼티를 보유한 GdkWindowAttr 객체가 필요하다. 표 11-3은 GtkWindowAttr 구조체의 모든 member를 설명하고 있다.
변수 | 설명 |
gchar *title | 창의 제목. 최상위 수준의 창이 아닐 경우에는 NULL. 보통은 이 값을 설정할 필요가 없다. |
gint event_mask | 위젯이 실현하게 될 GDK 이벤트의 bitmask. gtk_widget_get_events()를 이용해 현재 위젯에 연관된 모든 이벤트를 검색한 후 자신만의 이벤트를 추가할 수 있다. |
gint x, y | 부모 창을 기준으로 GdkWindow 객체의 x와 y 좌표. 위젯의 할당으로부터 이 값을 검색할 수 있다. |
gint width, height | GdkWindow 객체의 너비와 높이. 위젯의 할당에서 이 값을 검색할 수 있다. |
GdkWindowClass wclass | 대부분의 GdkWindow 객체에는 GDK_INPUT_OUTPUT으로 설정되고, 창이 눈에 보이지 않을 경우 GDK_INPUT_ONLY로 설정되어야 한다. |
GdkVisual *visual | 창에 사용될 GdkVisual 객체. 기본값은 gtk_widget_get_visual()로 검색 가능하다. |
GdkColormap *colormap | 창에 사용될 GdkColormap 객체. 기본값은 gdk_widget_get_colormap()으로 검색 가능하다. |
GdkWindowType window_type | 열거에 정의된 대로 표시될 창 타입. |
GdkCursor *cursor | 마우스가 위젯 위에 있을 때 표시될 선택적 GdkCursor 객체. |
gchar *wmclass_name | 이 프로퍼티는 무시해야 한다. 더 많은 정보는 gtk_window_set_wmclass()에 관한 문서를 참조한다. |
gchar *wmclass_class | 이 프로퍼티는 무시해야 한다. 더 많은 정보는 gtk_window_set_wmclass()에 관한 문서를 참조한다. |
gboolean override_redirect | TRUE로 설정 시 위젯은 창 관리자를 건너뛸(bypass) 것이다. |
표 11-3. GtkWindowAttr Members |
my_marquee_realize()의 구현에서 우리는 먼저 부모 창의 상단 좌측 모서리를 기준으로 위젯의 가로 및 세로 위치를 설정하였다. 위젯의 할당에서 이미 그 위치를 제공하기 때문에 이 작업은 수월하다. 할당은 위젯의 초기 너비와 높이도 제공한다.
다음 member인 wclass는 두 개의 값 중 하나로 설정된다. GDK_INPUT_OUTPUT은 일반적인 GdkWindow 위젯을 나타내어서 대부분의 위젯에 사용된다. GDK_INPUT_ONLY는 이벤트를 수신하는 데에 사용되는 눈에 보이지 않는 GdkWindow 위젯에 해당한다. 그 다음으로 개발자는 창 타입을 설정할 수 있는데, 이는 아래의 GdkWindowType 열거에서 정의된 값으로 결정된다.
- GDK_WINDOW_ROOT: 부모 창이 없는 창으로, 전체 화면을 차지할 것이다. 창 관리자(window manager)만 이 값을 사용할 수 있다.
- GDK_WINDOW_TOPLEVEL: 보통 데코레이션이 있는 최상위 수준의 창이다. 이러한 창 타입을 사용하는 창의 예로 GtkWindow를 들 수 있다.
- GDK_WINDOW_CHILD: 최상위 수준 창의 자식 창이거나 어떤 자식 창의 자식 창에 해당한다. 최상위 수준 창이 아닌 대부분의 위젯에 사용된다.
- GDK_WINDOW_DIALOG: 이 타입은 오래되어 더 이상 사용되지 않으므로 사용해선 안 된다.
- GDK_WINDOW_TEMP: GtkMenu 위젯처럼 임시적으로만 표시될 창이다.
- GDK_WINDOW_FOREIGN: GdkWindow 위젯으로 래핑되어야 하는 다른 라이브러리에서 구현한 이질적인 창 타입이다.
다음 호출은 GdkWindow에 대한 이벤트 마스크를 설정한다. gtk_widget_get_events()를 호출하면 위젯에 이미 설치된 이벤트를 모두 리턴하고 GDK_EXPOSURE_MASK를 리스트로 추가한다. 그래야만 노출 함수가 호출되도록 확보할 수 있을 것이다.
다음으로 GdkWindow 위젯에 사용될 GdkVisual 객체를 설정한다. 이 객체는 비디오 하드웨어에 관한 구체적인 정보를 설명하는 데에 사용된다. 대부분의 경우 위젯으로 할당된 기본 GdkVisual을 사용하길 원할 것인데, 이는 gtk_widget_get_visual()을 이용해 검색할 수 있다.
GdkWindowAttr 구조체에서 마지막으로 설정되는 프로퍼티는 색상 맵이다. 다시 말하지만 맵을 편집할 일은 별로 없을 것이기 때문에 gdk_widget_get_colormap()을 이용해 위젯에 대한 기본 맵을 검색하였다.
다음 단계는 GdkWindowAttr에서 어떤 필드가 honor되어야 하는지를 나타내는 구체적인 GdkWindowAttributesType 값의 마스크를 생성하는 일이다. 이번 예제에서는 명시된 x와 y 좌표인 GdkVisual, 그리고 GdkColormap이 사용될 것이다.
attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL | GDK_WA_COLORMAP;
이제 gdk_window_new()를 이용해 위젯에 대한 새로운 GdkWindow를 생성하기에 충분한 정보가 모였다. 이 함수는 부모 GdkWindow, GdkWindowAttr 객체, 그리고 honor해야 할 속성의 마스크를 수락한다.
GdkWindow* gdk_window_new (GdkWindow *parent,
GdkWindowAttr *attributes,
gint attributes_mask);
다음으로 gdk_window_set_user_data()를 이용해 GtkWidget을 커스텀 위젯에 대한 GdkWidget의 사용자 데이터로 저장해야 한다. 그래야만 expose-event와 같은 위젯 이벤트가 확실히 실현되도록 보장할 수 있다. 이 함수를 호출하지 않으면 이벤트는 실현되지 않을 것이다.
void gdk_window_set_user_data (GdkWindow *window,
gpointer user_data);
창의 스타일은 gtk_style_attach()를 이용해 창으로 추가되는데, 이 함수를 호출하면 스타일을 위한 그래픽을 생성하는 과정이 시작된다. 리턴된 값이 새로운 스타일일 수도 있으니 항상 저장할 것을 명심한다.
GtkStyle* gtk_style_attach (GtkStyle *style,
GdkWindow *window);
스타일이 창으로 추가되고 나면 창의 배경색이 설정된다. gtk_style_set_background()는 GdkWindow의 배경색을 주어진 상태로 된 GtkStyle이 명시한 색상대로 설정한다.
void gtk_style_set_background (GtkStyle *style,
GdkWindow *window,
GtkStyleType state_type);
마지막으로 gtk_window_show()를 호출하여 창이 사용자에게 표시된다. 이 함수를 호출하지 않으면 위젯은 사용자에게 절대로 표시되지 않을 것이다. 이 함수는 모든 필요한 초기화가 실행되도록 보장한다.
크기 요청과 크기 할당 명시하기
우리는 부모 GtkWindowClass의 크기 요청과 할당 함수도 오버라이드하였다. 리스팅 11-23에 실린 my_marquee_size_request() 함수는 크기 요청에 대한 기본 너비 및 높이 값을 명시하는 데에 사용되었다.
리스팅 11-23. 크기 요청과 할당 처리하기
/* Handle size requests for the widget. This function forces the widget to have
* an initial size set according to the predefined width and the font size. */
static void
my_marquee_size_request (GtkWidget *widget,
GtkRequisition *requisition)
{
PangoFontDescription *fd;
g_return_if_fail (widget != NULL || requisition != NULL);
g_return_if_fail (IS_MY_MARQUEE (widget));
fd = widget->style->font_desc;
requisition->width = MARQUEE_MIN_WIDTH;
requisition->height = (pango_font_description_get_size (fd) / PANGO_SCALE) + 10;
}
/* Handle size allocations for the widget. This does the actual resizing of the
* widget to the requested allocation. */
static void
my_marquee_size_allocate (GtkWidget *widget,
GtkAllocation *allocation)
{
MyMarquee *marquee;
g_return_if_fail (widget != NULL || allocation != NULL);
g_return_if_fail (IS_MY_MARQUEE (widget));
widget->allocation = *allocation;
marquee = MY_MARQUEE (widget);
if (GTK_WIDGET_REALIZED (widget))
{
gdk_window_move_resize (widget->window, allocation->x, allocation->y,
allocation->width, allocation->height);
}
}
크기 요청 함수는 초기 너비를 MARQUEE_MIN_WIDTH로 설정하는데, 이는 파일의 맨 위에서 설정되었다. 뿐만 아니라 높이가 글꼴 높이에 최소 10 픽셀을 더한 값이 되도록 강제로 설정한다. 그래야만 위젯에 일부 패딩과 함께 모든 메시지가 표시되도록 보장할 수 있다.
리스팅 11-23의 할당 함수는 주어진 할당을 위젯으로 할당하면서 시작된다. 위젯이 실현되면 gdk_window_move_resize()를 호출한다. 이 함수는 GdkWindow의 크기를 조정하여 하나의 셀로 이동시키는 데에 사용할 수 있다. 그리고 창의 새로운 x 좌표, y 좌표, 너비, 높이를 비롯해 작업해야 할 GdkWindow도 수락한다.
void gdk_window_move_resize
(GdkWindow *window,
gint x,
gint y,
gint width,
gint height);
위젯 노출하기
my_marquee_expose() 함수에서 매우 흥미로운 일이 발생한다. 위젯이 먼저 사용자에게 표시될 때, 위젯의 크기가 조정되었을 때, 이전에 숨겼던 창의 일부가 표시될 때 이 함수가 호출된다. 이 함수를 리스팅 11-24에 소개하였다.
리스팅 11-24. MyMarquee 위젯 노출하기
static gint
my_marquee_expose (GtkWidget *widget,
GdkEventExpose *event)
{
PangoFontDescription *fd;
MyMarquee *marquee;
MyMarqueePrivate *priv;
PangoLayout *layout;
PangoContext *context;
gint width, height;
g_return_val_if_fail (widget != NULL || event != NULL, FALSE);
g_return_val_if_fail (IS_MY_MARQUEE (widget), FALSE);
if (event->count > 0)
return TRUE;
marquee = MY_MARQUEE (widget);
priv = MY_MARQUEE_GET_PRIVATE (marquee);
fd = widget->style->font_desc;
context = gdk_pango_context_get ();
layout = pango_layout_new (context);
g_object_unref (context);
/* Create a new PangoLayout out of the message with the given font. */
pango_layout_set_font_description (layout, fd);
pango_layout_set_text (layout, priv->message, -1);
pango_layout_get_size (layout, &width, &height);
/* Clear the text from the background of the widget. */
gdk_window_clear_area (widget->window, 0, 0, widget->allocation.width,
widget->allocation.height);
/* Draw the PangoLayout on the widget, which is the message text. */
gdk_draw_layout (widget->window,
widget->style->fg_gc[widget->state],
priv->current_x,
( widget->allocation.height - (height / PANGO_SCALE)) / 2,
layout);
return TRUE;
}
pango_layout_new()를 이용해 새로운 PangoLayout을 생성하는 일부터 시작했다. 이 레이아웃은 위젯에 텍스트를 그릴 때에 사용될 것이다. 해당 함수는 PangoContext 객체를 수락하는데, 기본 컨텍스트는 gdk_pango_context_get()을 이용해 검색하였다.
PangoLayout* pango_layout_new (PangoContext *context);
이러한 PangoLayout의 구현은 매우 간단하다. pango_layout_set_text()를 호출하면 레이아웃의 텍스트 내용이 MyMarquee 위젯의 message 프로퍼티로 설정된다. 텍스트의 너비와 높이는 pango_layout_get_size()를 호출하여 검색된다.
pango_layout_get_size()가 리턴한 너비와 높이 값은 PANGO_SCALE에 의해 스케일링된다. 따라서 그 값을 픽셀로 얻기 위해서는 정수를 스케일(scale)로 나누어야 할 것이다.
PangoLayout이 준비되고 나면 전체 위젯이 제거되고 위젯을 그릴 준비를 시킨다. 이 때 gdk_window_clear_area()를 호출하면 (x,y) 좌표부터 (x + widget,y + height)까지 영역을 제거한다.
void gdk_window_clear_area (GdkWindow *window,
gint x,
gint y,
gint width,
gint height);
영역을 제거하고 나면 gdk_draw_layout()을 이용해 화면에 레이아웃을 그릴 수 있다. 이 함수는 먼저 그려야 할 GdkDrawable 객체, 즉 GdkWindow를 수락한다. 두 번째 매개변수는 사용할 그래픽 컨텍스트로, 클래스의 GtkStyle member에 의해 저장된다.
void gdk_draw_layout (GdkDrawable *drawable,
GdkGC *gc,
gint x,
gint y,
PangoLayout *layout);
마지막으로 레이아웃을 그릴 x와 y 좌표를 명시해야 한다. 이 위치들은 창 안에 있을 필요가 없음을 기억하라. 처음에는 레이아웃이 위젯의 우측에 그려지기 때문에 왼쪽으로 스크롤이 가능하다. 좌측의 뷰에서 완전히 숨겨지고 나서야 초기 위치로 리셋될 것이다. 따라서 스케일링 주기(scrolling cycle) 끝에 x 좌표는 사실상 음수가 될 것이다.
그리기 함수
PangoLayout 객체를 GdkWindow 객체로 그리는 기능 외에도 GDK는 GdkDrawable 객체를 통해 수많은 기본 그리기 함수를 제공한다. 전체 함수의 리스트는 GDK API 문서에서 찾아볼 수 있다. 표 11-4에는 필요한 함수를 쉽게 찾을 수 있도록 몇 가지를 열거하였다
함수 | 설명 |
gdk_draw_arc() | (x,y)에서 시작해 (x + width,y + height)에서 끝나는 호(arc)를 그린다. 호에 색상을 채울 것인지 선택할 수 있다. 시작 각도와 끝 각도를 1/64도로 명시해야 한다. |
gdk_draw_drawable() | 때로는 자신의 GdkDrawable에 그리기가 가능한 영역의 특정 일부를 복사하는 것이 바람직하다. 이 함수는 복사할 영역에서 그리기 가능한 소스의 영역을 명시하도록 해준다. |
gdk_draw_image() | 소스 GdkImage 객체의 일부를 그리기가 가능한 영역에 그린다. GdkDrawable 객체를 GdkImage 객체로 변환할 수 있으므로 이 함수는 사실상 그리기가 가능한 소스가 될 수 있다. |
gdk_draw_layout() | PangoLayout이 정의한 구체적인 텍스트의 문자 수만큼 그린다. GdkDrawable에 텍스트를 위치시킬 때 사용된다. |
gdk_draw_layout_line() | gdk_draw_layout()과 비슷하지만 PangoLayout에서 PangoLayoutLine이라는 단일 행을 그릴 수 있다는 점에 차이가 있다. |
gdk_draw_line() | 시작점부터 끝점까지 직선을 그린다. 그래픽 컨텍스트의 전경색을 이용해 그려질 것이다. |
gdk_draw_lines() | GdkPoint 배열에 명시된 끝점을 이용해 일련의 선을 그린다. 배열 내 점의 개수를 명시해야 한다. |
gdk_draw_pixbuf() | GdkDrawable 객체에 GdkPixbuf 이미지의 일부를 그린다. 이미지를 렌더링할 때 이용될 추가 매개변수도 명시해야 한다. |
gdk_draw_point() | 그래픽 컨텍스트에 명시된 전경색을 이용해 화면에 하나의 점을 그린다. 개발자는 점에 대한 x 좌표와 y 좌표를 제공하면 된다. |
gdk_draw_points() | GdkPoint 객체의 배열에 명시된 다수의 점을 화면으로 그린다. GdkPoint 구조체는 x 와 y 좌표를 보유한다. 개발자는 배열에서 점의 개수 또한 명시해야 한다. |
gdk_draw_polygon() | GdkPoint 객체의 배열에 열거된 점을 연결하는 다각형을 그린다. 필요하다면 마지막 점이 처음 점으로 연결될 것이다. 다각형의 채움 여부를 선택할 수 있다. |
gdk_draw_rectangle() | gdk_draw_polygon()과 유사하지만 결과적인 모양이 항상 직사각형이라는 점에서 차이가 있다. 개발자는 y 좌표, y 좌표, 너비, 높이를 비롯해 직사각형의 채움 여부를 명시해야 한다. |
gdk_draw_segments() | 연결되지 않은 라인 세그먼트(line segment)를 여러 개 그린다. 각 라인 세그먼트는 시작 좌표와 끝 좌표를 보유하는 GdkSegment 객체에 저장된다. 이 함수에 GdkSegment 객체의 배열이 제공된다. |
gdk_draw_trapezoids() | GdkTrapezoid 객체의 배열에 저장된 부등변 사각형(trapezoid)을 여러 개 그린다. GdkTrapezoid 구조체는 시작점과 끝점에 대한 y 좌표를 보유한다. 부등변 사각형의 각 모서리에 해당하는 4개의 x 좌표 또한 보유한다. |
표 11-4. GdkDrawable 함수 |
Public 함수 구현하기
MyMarquee 위젯에는 다수의 public 함수가 포함되어 있다. 그 중에서 가장 중요한 함수는 my_marquee_slide()인데, 이 함수를 호출하면 메시지를 speed 픽셀만큼 좌측으로 이동시킬 것이다. 프로그래머는 이 함수를 timeout으로 추가함으로써 명시된 시간 간격으로 함수를 호출하여 marquee 효과를 야기할 수 있다.
리스팅 11-25. MyMarquee 메시지 슬라이딩하기
void
my_marquee_slide (MyMarquee *marquee)
{
PangoFontDescription *fd;
GtkWidget *widget;
MyMarqueePrivate *priv;
PangoLayout *layout;
PangoContext *context;
gint width, height;
g_return_if_fail (marquee != NULL);
g_return_if_fail (IS_MY_MARQUEE (marquee));
widget = GTK_WIDGET (marquee);
priv = MY_MARQUEE_GET_PRIVATE (marquee);
fd = widget->style->font_desc;
context = gdk_pango_context_get ();
layout = pango_layout_new (context);
g_object_unref (context);
/* Create a new PangoLayout out of the message with the given font. */
pango_layout_set_font_description (layout, fd);
pango_layout_set_text (layout, priv->message, -1);
pango_layout_get_size (layout, &width, &height);
/* Clear the text from the background of the widget. */
gdk_window_clear_area (widget->window, 0, 0, widget->allocation.width,
widget->allocation.height);
/* Scroll the message "speed" pixels to the left or wrap around. */
priv->current_x = priv->current_x - priv->speed;
if ((priv->current_x + (width / PANGO_SCALE)) <= 0)
priv->current_x = widget->allocation.width;
/* Draw the PangoLayout on the widget, which is the message text. */
gdk_draw_layout (widget->window,
widget->style->fg_gc[widget->state],
priv->current_x,
(widget->allocation.height - (height / PANGO_SCALE)) / 2,
layout);
}
이 함수는 앞서 구현한 노출 함수와 매우 유사함을 눈치챌 것이다. 두 함수 간 차이점을 살펴보자.
이 함수는 텍스트의 새로운 위치, 즉 현재 위치에서 speed 프로퍼티 값을 뺀 값을 계산해야 한다. 다음으로 메시지가 화면에 여전히 표시되는지 확인해야 한다. 위젯의 왼쪽 범위 밖으로 이동했다면 위치는 위젯의 우측 면으로 리셋되어 스크롤링 메시지를 순환(loop)한다. 이 시점에서는 my_marquee_expose()에서와 동일한 방식으로 그리기가 이루어진다.
마지막으로 우리는 MyMarquee 위젯의 speed 와 message 프로퍼티를 설정 및 검색하는 기능을 제공해야 한다. 이러한 프로퍼티로 접근하려면 MY_MARQUEE_GET_PRIVATE()을 이용해 private 데이터 구조체를 검색해야 함을 기억해야 한다.
리스팅 11-26. Message와 Speed를 설정하고 검색하기
/* Set the message that is displayed by the widget. */
void
my_marquee_set_message (MyMarquee *marquee,
const gchar *message)
{
MyMarqueePrivate *priv = MY_MARQUEE_GET_PRIVATE (marquee);
if (priv->message)
{
g_free (priv->message);
priv->message = NULL;
}
priv->message = g_strdup (message);
}
/* Retrieve the message that is displayed by the widget. You must free this
* string after you are done using it! */
gchar*
my_marquee_get_message (MyMarquee *marquee)
{
return g_strdup (MY_MARQUEE_GET_PRIVATE (marquee)->message);
}
/* Set the number of pixels that the message will scroll. */
void
my_marquee_set_speed (MyMarquee *marquee,
gint speed)
{
MyMarqueePrivate *priv = MY_MARQUEE_GET_PRIVATE (marquee);
priv->speed = speed;
}
/* Retrieve the number of pixels that the message will scroll. */
gint
my_marquee_get_speed (MyMarquee *marquee)
{
return MY_MARQUEE_GET_PRIVATE (marquee)->speed;
}
위젯 테스트하기
위젯 소스가 작성되었으니 이제 위젯을 테스트할 때가 되었다. 작은 테스트 애플리케이션을 리스팅 11-27에 싣겠다. Timeout이 추가되어 150 밀리 초마다 my_marquee_slide()를 호출할 것이다.
marquee는 "Wheeeee!"라는 초기 메시지로 설정되었고, my_marquee_slide()를 호출할 때마다 좌측으로 10 픽셀만큼 이동할 것이다.
리스팅 11-27. MyMarquee 위젯 테스트하기 (marqueetest.c)
#include <gtk/gtk.h>
#include "mymarquee.h"
int main (int argc,
char *argv[])
{
GtkWidget *window, *marquee;
PangoFontDescription *fd;
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (window), "MyMarquee");
gtk_container_set_border_width (GTK_CONTAINER (window), 10);
g_signal_connect (G_OBJECT (window), "destroy",
G_CALLBACK (gtk_main_quit), NULL);
fd = pango_font_description_from_string ("Monospace 30");
marquee = my_marquee_new ();
gtk_widget_modify_font (marquee, fd);
my_marquee_set_message (MY_MARQUEE (marquee), "Wheeeee!");
my_marquee_set_speed (MY_MARQUEE (marquee), 10);
pango_font_description_free (fd);
g_timeout_add (150, (GSourceFunc) my_marquee_slide, (gpointer) marquee);
gtk_container_add (GTK_CONTAINER (window), marquee);
gtk_widget_show_all (window);
gtk_main ();
return 0;
}
인터페이스 구현하기
앞의 여러 장에 걸쳐 GtkEditable, GtkFileChooser, GtkTreeModel, GtkRecentChooser를 포함해 여러 가지 인터페이스를 소개하였다. GObject의 인터페이스는 Java의 것과 매우 비슷하다. 새로운 인터페이스는 리스팅 11-28에서 볼 수 있듯이 GTypeInterface로부터 상속된다.
이번 절에 실린 코드는 인터페이스를 사용 시 꼭 필요한 것이 무엇인지 설명하기 위해 매우 기본적인 인터페이스와 객체를 구현한다. 실용적으로 사용하기 위해서는 훨씬 더 많은 API를 포함하도록 크게 확장되어야 할 것이다.
리스팅 11-28. 인터페이스 헤더 파일 (myiface.h)
#ifndef __MY_IFACE_H__
#define __MY_IFACE_H__
#include <gtk/gtk.h>
G_BEGIN_DECLS
#define MY_TYPE_IFACE (my_iface_get_type ())
#define MY_IFACE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
GTK_TYPE_IFACE, MyIFace))
#define MY_IS_IFACE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
GTK_TYPE_IFACE))
#define MY_IFACE_GET_INTERFACE(inst) (G_TYPE_INSTANCE_GET_INTERFACE ((inst), \
MY_TYPE_IFACE, MyIFaceInterface))
typedef struct _MyIFace MyIFace;
typedef struct _MyIFaceInterface MyIFaceInterface;
struct _MyIFaceInterface
{
GTypeInterface parent;
void (*print_message) (MyIFace *obj, gchar *message);
};
GType my_iface_get_type ();
void my_iface_print_message (MyIFace *obj, gchar *message);
G_END_DECLS
#endif /* __MY_IFACE_H__ */
myiface.h 헤더 파일에는 새로운 위젯을 생성할 때와 동일한 함수와 구조체가 상당히 많이 포함되어 있음을 눈치챌 것이다. 정의부는 네 개가 있는데 이들은 각각 인터페이스의 GType을 리턴하고, 인터페이스를 캐스팅하고, 유효한 GTK_TYPE_IFACE인지 확인하며, 연관된 인터페이스를 리턴한다.
인터페이스를 선언할 때는 MyIFace 구조체에 대한 타입 정의를 선언해야 하지만 이는 사실 MY_IFACE()가 작동하도록 허용하는 opaque 타입에 불과하다. MyIFaceInterface 야말로 인터페이스의 실제 내용에 해당한다. 이는 모든 인터페이스의 부모 타입인 GTypeInterface 객체를 포함해야 한다.
이는 하나 또는 이상의 함수 포인터를 포함하기도 한다. 객체가 주어진 인터페이스를 구현하면 프로그래머는 이러한 함수들을 오버라이드한다. 그래야만 각 객체가 고유의 방식으로 인터페이스를 구현하는 동시 여러 객체들 간 명명 규칙에 일관성을 제공할 수 있다.
인터페이스 구현하기
리스팅 11-29는 매우 기본적인 MyIFace 소스 파일의 구현을 싣고 있다. 이는 새로운 인터페이스 GType의 등록, 인터페이스 클래스의 초기화, member 함수의 호출을 위한 함수들을 제공한다.
리스팅 11-29. 인터페이스 소스 파일 (myiface.c)
#include "myiface.h"
static void my_iface_class_init (gpointer iface);
GType
my_iface_get_type ()
{
static GType type = 0;
if (!type)
{
type = g_type_register_static_simple (G_TYPE_INTERFACE, "MyIFace",
sizeof (MyIFaceInterface),
(GClassInitFunc) my_iface_class_init,
0, NULL, 0);
g_type_interface_add_prerequisite (type, GTK_TYPE_WIDGET);
}
return type;
}
static void
my_iface_class_init (gpointer iface)
{
GType iface_type = G_TYPE_FROM_INTERFACE (iface);
/* Install signals & properties here ... */
}
void
my_iface_print_message (MyIFace *obj,
gchar *message)
{
MY_IFACE_GET_INTERFACE (obj)->print_message (obj, message);
}
리스팅 11-29의 첫 번째 함수는 MyIFace 타입을 등록하기 위해 사용되었다. 이 때 g_type_register_static_simple()을 이용하였다. 이 함수는 먼저 부모에 해당하는 GType과 새로운 타입의 이름을 수락한다. 부모 타입은 인터페이스에 대한 G_TYPE_INTERFACE에 해당한다. 세 번째 매개변수는 인터페이스 구조체의 크기로, sizeof() 함수를 이용해 얻을 수 있다.
GType g_type_register_static_simple (GType parent_type,
const
gchar *type_name,
guint class_size,
GClassInitFunc class_init,
guint instance_size,
GInstanceInitFunc instance_init,
GTypeFlags flags);
다음으로는 클래스 초기화 함수를 명시해야 한다. 인스턴스 크기와 인스턴스 초기화 함수 모두 무시될 수도 있는데, 인스턴스 구조체가 opaque 타입이기 때문이다. 마지막 매개변수는 GTypeFlags의 bitwise 필드로, 인터페이스에 대해 안전하게 0으로 설정 가능하다.
인터페이스를 구현하는 객체라면 prerequisite_type 또한 강제로 구현하도록 g_type_interface_add_prerequisite() 라는 함수를 이용하였다. 인터페이스는 기껏해야 하나의 전제조건(prerequisite)만 가질 수 있다.
void g_type_interface_add_prerequisite (GType interface_type,
GType prerequisite_type);
클래스 초기화 함수는 여느 GObject 클래스 초기화 함수와 다를 게 없다. 인터페이스가 필요로 하는 시그널과 프로퍼티를 준비할 때는 이 함수를 이용해야 한다. 이러한 함수들을 인터페이스로 추가한다는 것은 이 인터페이스를 구현하는 클래스라면 어디서든 이용할 수 있을 것이란 의미다.
마지막 함수 my_iface_print_message()는 단순히 현재 MyIFaceInterface 인스턴스에 위치한 함수를 호출하는 public 함수다. 이는 인터페이스를 구현하고 있는 객체에 의해 추가된 함수의 인스턴스를 호출할 것이란 의미다.
인터페이스 사용하기
객체에서 인터페이스를 구현하기란 매우 간단한 작업이다. 가장 먼저 자신의 GType 등록 함수에 두 가지를 추가하면 된다. 리스팅 11-30에는 MyObject라는 가상의 클래스에 이 함수를 사용하는 예제를 실었다. 이 객체는 인터페이스의 사용법이 얼마나 쉬운지만 보이도록 객체의 가장 기본 요소들만 포함한다.
리스팅 11-30. 객체의 GType 생성하기
GType
my_object_get_type (void)
{
static GType type = 0;
if (!type)
{
static const GTypeInfo info =
{
sizeof (MyObjectClass),
NULL,
NULL,
(GClassInitFunc) my_object_class_init,
NULL,
NULL,
sizeof (MyObject),
0,
(GInstanceInitFunc) my_object_init,
};
static const GInterfaceInfo iface_info =
{
(GInterfaceInitFunc) my_object_interface_init,
NULL,
NULL
};
type = g_type_register_static (GTK_TYPE_WIDGET, "MyObject", &info, 0);
g_type_add_interface_static (type, MY_TYPE_INTERFACE, &iface_info);
}
return type;
}
이 함수는 가장 먼저 GInterfaceInfo 객체를 선언한다는 점에서 여태까지 소개한 함수와 다르다. 이 구조체는 세 가지 정보 조각을 보유한다. 두 개의 member는 인터페이스가 초기화될 때와 최종화될 때 호출되는 GInterfaceInitFunc와 GInterfaceFinalizeFunc 함수다. 세 번째 member는 각 함수로 전달되는 데이터의 포인터다. 처음 두 개의 member는 무시해도 안전하다.
두 번째 차이점은 인터페이스를 인스턴스 타입으로 추가하는 데에 사용되는 g_type_add_interface_static()을 호출한다는 데에 있다. 이 함수는 세 개의 매개변수를 수락하는데, 바로 인스턴스 GType, 인터페이스 GType, 이전에 정의된 GInterfaceInfo 객체가 그것이다.
void g_type_add_interface_static (GType instance_type,
GType interface_type,
const GInterfaceInfo *info);
리스팅 11-31은 MyIFace 인터페이스를 구현하는 과정의 마지막 두 단계를 보여준다. 첫 번째 함수 my_object_print_message()는 MyIFaceInterface member가 가리킬 print_message() 함수의 실제 구현이다. 해당 함수는 프로그래머가 my_iface_print_message()를 호출하면 호출될 것이다.
리스팅 11-31. 인터페이스 초기화하기
static void
my_object_print_message (MyObject *object,
gchar *message)
{
g_print (message);
}
static void
my_object_interface_init (gpointer iface,
gpointer data)
{
MyIFaceInteface *iface = (MyIFaceInterface*) iface;
iface->print_message =
(void (*) (MyIFace *obj, gchar *message)) my_object_print_message;
}
리스팅 11-31에 실린 두 번째 함수는 객체의 인터페이스 초기화 함수의 구현이다. 이는 단순히 MyIFaceInterface의 print_message() member를 함수에 대한 객체의 구현으로 가리킬 뿐이다.
위의 리스팅은 매우 간단한 인터페이스 구현 예제에 해당하지만 좀 더 복잡한 예제를 만들 때 필요한 기본 요소들을 모두 가르쳐준다. 지금쯤이면 다른 GObject에서 자신만의 객체를 상속하는 법을 비롯해 자신만의 인터페이스를 생성 및 구현할 수 있을 것인데, 이 정도면 꽤 큰 성과가 아닌가! 다음 장에서는 GTK+에 이미 빌드되어 있는 위젯에 관한 학습으로 다시 돌아가겠다.
자신의 이해도 시험하기
이번 장에 실린 연습문제에서는 몇 가지 새로운 기능을 포함하도록 MyMarquee 위젯을 확장시킬 것이다. 이를 위해서는 코드의 많은 부분을 편집하고 API 문서의 새로운 함수들을 살펴볼 필요가 있다. 또 message-changed 시그널과 같이 연습문제에 실리진 않았지만 위젯에 자신만의 개선사항(enhancement)을 추가하는 것도 고려해보도록 한다!
연습문제 11-1. MyMarquee 확장하기
이번 연습문제에서는 MyMarquee에 몇 가지 새로운 기능을 확장한다. 먼저 프로그래머는 스크롤 방향이 좌측인지 우측인지 명시할 수 있어야 한다. 또 위젯 주변에 직사각형으로 된 테두리를 위치시켜라. 그 외 프로퍼티인 메시지는 이제 순환(cycled)되는 메시지의 리스트여야 한다. 초기 메시지는 my_marquee_new()에서 설정할 수 있어야 한다.
그에 이어 마우스가 위젯의 주변으로 들어서면 호출되는 오버라이드 함수를 구현하라. 마우스가 위젯 주위로 들어가면 마우스 커서가 그 주위를 벗어날 때까지 메시지의 스크롤이 중단되어야 한다. 이를 구현하기 위해서는 GdkWindow 객체에 새로운 이벤트 마스크를 추가해야 할 것이다.
요약
이번 장에서는 새로운 객체를 상속하는 방법을 가르치기 위해 두 가지 예를 살펴보았다. 처음에 소개한 예제는 GtkEntry에서 상속된 MyIPAddress라는 새로운 위젯을 생성하였다. 두 번째 새로운 위젯은 MyMarquee 위젯으로서 화면에 걸쳐 메시지를 스크롤한다. 이 예제는 화면에 위젯을 한 부분씩 그림으로써 말 그대로 처음부터 새 위젯을 생성하는 방법을 가르쳐주었다.
다음으로 GTK+에서 인터페이스를 구현하고 사용하는 방법을 소개하였다. 이를 통해 개발자는 본인이 생성한 새로운 위젯에 이미 존재하는 인터페이스를 사용하거나 자신만의 인터페이스를 생성할 수 있다.
다음 장에서는 앞 장에서 다루지 않았던 위젯들을 다수 학습할 것이다. 이러한 위젯으로는 인쇄(printing) 위젯, 최근 파일 지원, 캘린더(calendar), 자동 완성 엔트리, 상태 아이콘, 그리기 영역이 있다.