FoundationsofGTKDevelopment:Chapter 07
- 제 7 장 텍스트 뷰 위젯
텍스트 뷰 위젯
제 6장에서는 GLib가 제공하는 수많은 유틸리티와 데이터 구조체, 그리고 기능의 유형을 학습하였기 때문에 나머지 본문을 통해 학습하게 될 GLib는 이제 얼마 남지 않았다. 대신 제 6장에서 얻은 지식을 앞으로 보게 될 예제와 연습문제에 적용할 것이다.
제 7장에서는 GtkTextView 위젯을 사용하는 방법을 안내하고자 한다. 텍스트 뷰 위젯은 여러 행에 이어지는 텍스트를 보유할 수 있다는 점만 제외하고 GtkEntry 위젯과 유사하다. 스크롤이 있는 창을 이용하여 문서가 화면의 범위를 벗어나서도 존재할 수 있도록 만들 것이다.
GtkTextView를 학습하기 전에 제 7장은 몇 가지 새로운 위젯의 소개로 시작하겠다. 가장 먼저 소개할 두 위젯은 바로 스크롤을 이용한 창과 뷰포트(viewport)다. 스크롤을 이용한 창은 자식 위젯을 스크롤하는 데에 사용되는 두 개의 스크롤바로 구성된다. GtkLayout, GtkTreeView, GtkTextView를 포함해 몇 가지 위젯은 이미 스크롤을 지원한다. 이를 제외한 위젯에서 스크롤을 이용하고자 할 경우 GtkViewport 위젯에 먼저 추가하면 자식 위젯에게 스크롤 기능을 제공할 것이다.
이번 장에서는 다음과 같은 내용을 학습할 것이다.
- 스크롤이 있는 창과 뷰포트를 이용하는 방법
- GtkTextView 위젯을 사용하고 텍스트 버퍼를 적용하는 방법
- 버퍼를 처리할 때 텍스트 반복자와 텍스트 마크가 실행하는 함수
- 문서의 일부 또는 전체에 스타일을 적용하는 방법
- 클립보드로/클립보드로부터 자르기, 복사하기, 붙여넣기를 실행하는 방법
- 이미지와 자식 위젯을 텍스트 뷰로 삽입하는 방법
스크롤을 이용한 창
GtkTextView 위젯에 대해 학습하기 전에 GtkScrolledWindow와 GtkViewport라고 불리는 두 가지 컨테이너 위젯에 관해 배울 필요가 있겠다. 스크롤을 이용한 창은 두 개의 스크롤바를 이용해 위젯이 화면에 보이는 것 이상의 공간을 차지할 수 있도록 해준다. 이러한 위젯 덕분에 GtkTextView 위젯은 창 범위 이상으로 확장되는 문서를 포함할 수 있다.
스크롤이 있는 창에 포함된 두 개의 스크롤바 모두 연관된 GtkAdjustment 객체를 갖는다. 이러한 조정(adjustment)은 스크롤바의 범위와 현재 위치를 추적하는 데 사용된다. 하지만 대부분의 경우 조정 객체로 직접 접근하지 않아도 될 것이다.
typedef struct
{
gdouble value;
gdouble upper;
gdouble lower;
gdouble step_increment;
gdouble page_increment;
gdouble page_size;
} GtkAdjustment;
스크롤바의 GtkAdjustment는 스크롤 범위, 단계(step), 스크롤의 현재 위치에 관한 정보를 보유한다. value 변수는 범위 간 스크롤바의 현재 위치를 의미한다. 해당 변수는 항상 lower 값과 upper 값 사이에 있어야 하는데, 이 값들이 바로 조정의 범위에 해당한다. page_size는 위젯의 크기에 따라 화면에 한 번에 표시될 수 있는 영역이다. step_increment와 page_increment 변수들은 화살표를 누르거나 Page Down 키를 누를 때 실행되는 단계(stepping)에 사용된다.
그림 7-1은 리스팅 7-1에 실린 코드를 이용해 생성된 창의 스크린샷이다. 버튼을 포함하는 테이블이 눈에 보이는 영역보다 크기 때문에 두 개의 스크롤바 모두 활성화되었다.
리스팅 7-1은 스크롤을 이용한 창과 뷰포트를 사용하는 방법을 보여준다. 조정이 동기화되었기 때문에 스크롤바가 이동하면서 뷰포트도 함께 스크롤된다. 스크롤바가 자식 위젯보다 커지거나 작아질 때 어떻게 반응하는지 확인하려면 창의 크기를 조정해보라.
리스팅 7-1. 스크롤이 있는 창 이용하기 (scrolledwindows.c)
#include <gtk/gtk.h>
int main (int argc,
char *argv[])
{
GtkWidget *window, *swin, *viewport, *table1, *table2, *vbox;
GtkAdjustment *horizontal, *vertical;
GtkWidget *buttons1[10][10], *buttons2[10][10];
unsigned int i, j;
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (window), "Scrolled Windows & Viewports");
gtk_container_set_border_width (GTK_CONTAINER (window), 10);
gtk_widget_set_size_request (window, 500, 400);
g_signal_connect (G_OBJECT (window), "destroy",
G_CALLBACK (gtk_main_quit), NULL);
table1 = gtk_table_new (10, 10, TRUE);
table2 = gtk_table_new (10, 10, TRUE);
gtk_table_set_row_spacings (GTK_TABLE (table1), 5);
gtk_table_set_row_spacings (GTK_TABLE (table2), 5);
gtk_table_set_col_spacings (GTK_TABLE (table1), 5);
gtk_table_set_col_spacings (GTK_TABLE (table2), 5);
/* Pack each table with 100 buttons. */
for (i = 0; i < 10; i++)
{
for (j = 0; j < 10; j++)
{
buttons1[i][j] = gtk_button_new_from_stock (GTK_STOCK_CLOSE);
buttons2[i][j] = gtk_button_new_from_stock (GTK_STOCK_CLOSE);
gtk_button_set_relief (GTK_BUTTON (buttons1[i][j]), GTK_RELIEF_NONE);
gtk_button_set_relief (GTK_BUTTON (buttons2[i][j]), GTK_RELIEF_NONE);
gtk_table_attach_defaults (GTK_TABLE (table1), buttons1[i][j],
i, i + 1, j, j + 1);
gtk_table_attach_defaults (GTK_TABLE (table2), buttons2[i][j],
i, i + 1, j, j + 1);
}
}
/* Create a scrolled window and a viewport, each with one table. Use the
* adjustments in the scrolled window to synchronize both containers. */
swin = gtk_scrolled_window_new (NULL, NULL);
horizontal = gtk_scrolled_window_get_hadjustment (GTK_SCROLLED_WINDOW (swin));
vertical = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (swin));
viewport = gtk_viewport_new (horizontal, vertical);
gtk_container_set_border_width (GTK_CONTAINER (swin), 5);
gtk_container_set_border_width (GTK_CONTAINER (viewport), 5);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (swin),
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (swin), table1);
gtk_container_add (GTK_CONTAINER (viewport), table2);
/* Pack the widgets into a GtkVBox and then into the window. */
vbox = gtk_vbox_new (TRUE, 5);
gtk_box_pack_start_defaults (GTK_BOX (vbox), viewport);
gtk_box_pack_start_defaults (GTK_BOX (vbox), swin);
gtk_container_add (GTK_CONTAINER (window), vbox);
gtk_widget_show_all (window);
gtk_main();
return 0;
}
gtk_scrolled_window_new()를 이용하면 스크롤이 있는 창이 새로 생성된다. 리스팅 7-1에서 각 매개변수는 NULL로 설정되므로, 스크롤을 이용한 창은 두 개의 기본 조정이 생성되는 결과를 야기할 것이다. 대부분의 경우 개발자는 기본 조정의 사용을 원할 테지만 스크롤 바에 자신만의 수평 및 수직 조정을 명시하는 것도 가능하다.
이번 예제에서 조정은 gtk_viewport_new()를 통해 새로운 뷰포트가 생성될 때 사용된다. 뷰포트 조정은 스크롤이 있는 창의 조정을 이용해 초기화되어 두 개의 컨테이너가 동시에 스크롤되도록 확보한다.
가장 먼저 결정해야 할 내용은 스크롤을 이용한 창을 준비할 때 스크롤바를 언제 표시하는지에 관해서다. 예제에서는 두 가지 스크롤바에 모두 GTK_POLICY_AUTOMATIC이 사용되어 각각이 필요 시에만 표시된다. 두 스크롤바에서 모두 GTK_POLICY_ALWAYS가 기본 규칙(default policy)이 된다. GtkPolicyType이 제공하는 세 가지 열거 값은 다음과 같다.
- GTK_POLICY_ALWAYS: 스크롤바가 항상 표시될 것이다. 스크롤이 불가능할 경우 회색으로 표시되거나 disabled(비활성화됨)로 표시될 것이다.
- GTK_POLICY_AUTOMATIC: 스크롤이 가능할 때에만 스크롤바가 표시될 것이다. 필요하지 않다면 스크롤바는 임시적으로 사라질 것이다.
- GTK_POLICY_NEVER: 스크롤바가 절대 표시되지 않을 것이다.
많은 애플리케이션에서 사용되진 않지만 또 다른 프로퍼티로 스크롤바의 위치를 들 수 있다. 대부분의 애플리케이션에서 개발자는 스크롤바를 위젯의 하단이나 우측에 두고 싶어하며 이것이 사실상 기본값으로 설정된다.
하지만 이를 변경하기 위해서는 gtk_scrolled_window_set_placement()를 이용해야 한다. 해당 함수는 스크롤바를 기준으로 내용이 표시될 위치를 정의하는 GtkCornerType 값을 수신한다. 예를 들어, 내용은 보통 스크롤바를 기준으로 상단과 좌측에 표시되기 때문에 기본값은 GTK_CORNER_TOP_LEFT가 되겠다.
void gtk_scrolled_window_set_placement (GtkScrolledWindow *swin
GtkCornerType window_placement);
이용 가능한 GtkCornerType 값으로는 GTK_CORNER_TOP_LEFT, GTK_CORNER_BOTTOM_LEFT, GRK_CORNER_TOP_RIGHT, GTK_CORNER_BOTTOM_RIGHT가 있으며, 모두 스크롤바의 위치를 기준으로 내용을 표시하는 위치를 정의한다.
gtk_scrolled_window_set_placement()를 사용해야 하는 경우는 극히 드물다! 거의 모든 사례에서 이 함수는 사용자를 혼란스럽게 만들기 때문에 사용해선 안 된다. 위치를 변경해야 하는 특별한 이유가 없다면 기본값을 사용하라.
gtk_scrolled_window_set_shawdow_type()을 호출하면 자식 위젯에서 위젯의 그림자(shadow) 타입을 설정할 수 있다.
void gtk_scrolled_window_set_placement (GtkScrolledWindow *swin
GtkCornerType window_placement);
제 3장에서 우리는 자식 위젯 주변에 테두리 타입을 설정하기 위해 핸들 박스와 함께 GtkShadowType 열거의 사용법을 학습한 바 있다. 스크롤을 이용한 창의 그림자 타입을 설정 시에도 동일한 값이 사용된다.
스크롤을 이용한 창을 준비했다면 어디든 사용 가능하도록 자식 위젯을 추가해야 한다. 이를 실행하는 방법에는 두 가지가 있는데 자식 위젯의 타입을 기준으로 둘 중에 선택된다. GtkTextView, GtkTreeView, GtkIconView, GtkViewport, GtkLayout 위젯 중에서 하나를 사용 중이라면 기본 gtk_container_add() 함수를 이용해야 하는데, 해당하는 5개의 위젯 모두 네이티브 스크롤링 지원을 포함하기 때문이다.
다른 모든 GTK+ 위젯들은 네이티브 스크롤링을 지원하지 않는다. 이런 경우 gtk_scrolled_window_add_with_viewport()를 사용해야 한다. 해당 함수는 GtkViewport라고 불리는 컨테이너 위젯에 스크롤링 지원을 패킹하여 자식 위젯에게 제공한다. 이 위젯은 그러한 지원을 하지 않는 자식 위젯을 위해 스크롤링 기능을 구현한다. 이후 스크롤을 이용한 창에 뷰포트가 자동으로 추가된다.
GtkTextView, GtkTreeView, GtkIconView, GtkViewport, GtkLayout 중 어떤 것도gtk_scrolled_window_add_with_viewport()를 이용해 스크롤이 있는 창으로 패킹해서는 안 되는데 그 이유는 위젯에서 스크롤이 올바로 실행되지 않을 수도 있기 때문이다!
새로운 GtkViewport로 위젯을 수동으로 추가한 후 gtk_container_add()를 이용해 해당 뷰포트를 스크롤이 있는 창으로 추가하는 것도 가능하지만 편의 함수는 뷰포트를 완전히 무시할 수 있도록 해준다.
스크롤이 있는 창은 스크롤바가 포함된 컨테이너에 불과하다. 컨테이너와 스크롤은 어떠한 액션도 스스로 실행할 수 없다. 스크롤은 자식 위젯에 의해 처리되기 때문에 자식 위젯은 GtkScrolledWindow 위젯과 올바로 작동하도록 이미 네이티브 스크롤링을 지원한다.
스크롤링 지원을 가진 자식 위젯을 추가할 때는 각 축(axis)에 조정을 추가하도록 함수가 호출된다. 자식 위젯에 스크롤링이 지원되지 않는 한 어떤 일도 실행되지 않을 것인데, 이 때문에 대부분의 위젯이 뷰포트를 필요로 한다. 사용자가 스크롤바를 클릭하여 드래그하면 조정의 값이 변경되고, value-changed 시그널이 발생한다. 이러한 액션에 따라 자식 위젯은 스스로 렌더링되기도 한다.
GtkViewport 위젯은 고유의 스크롤바를 갖고 있지 않았기 때문에 화면에서 현재 위치를 정의하기 위해 전적으로 조정에 의존하였다. 조정의 현재 값을 조정하기 위한 간단한 매커니즘으로 GtkScrolledWindow 위젯에서 스크롤바가 사용된다.
텍스트 뷰
GtkTextView 위젯은 문서의 다중 행 텍스트를 표시하는 데 사용된다. 이는 문서 전체 또는 문서의 일부분을 맞춤설정하는 여러 방법을 제공한다. 문서에 GtkPixbuf 객체와 자식 위젯을 삽입하는 것도 가능하다. GtkTextView는 아마 지금까지 본 예제 중에 가장 타당하게 관련된 위젯일 것이므로 이번 장 나머지 부분에서는 해당 위젯의 많은 측면에 집중하여 살펴볼 것이다. 이는 많은 GTK+ 애플리케이션에서 사용하게 될 다용도의 위젯이다.
이번 장의 앞부분에 소개된 몇 가지 예제를 보고 어쩌면 GtkTextView 는 간단한 문서를 포함하는 것으로 그 사용이 제한되어 있다고 생각할 수도 있겠지만 사실이 아니다. 이는 다양한 애플리케이션에서 사용되는 대화형 문서, 워드 프로세싱, 많은 텍스트 타입을 표시할 때 사용할 수도 있다. 사용 방법은 잇따른 절에서 학습할 것이다.
그림 7-2는 GtkScrolledWindow 위젯이 포함하는 간단한 GtkTextView 위젯을 보여준다.
텍스트 뷰는 GTK+를 이용하는 모든 유형의 텍스트 및 문서 편집 애플리케이션에서 사용된다. AbiWord, Gedit, 또는 GNOME용으로 생성된 텍스트 에디터를 사용한 경험이 있다면 GtkTextView 위젯을 사용해본 셈이다. 이는 인스턴스 메시지 창 내 Gaim 애플리케이션에서도 사용된다. (사실 본문에 실린 모든 예제는 소스 코드 편집 시 GtkTextView를 이용하는 OpenLDev 애플리케이션에서 생성되었다!)
텍스트 버퍼
각 텍스트 뷰는 GtkTextBuffer라고 불리는 클래스의 내용을 표시하는 데 사용된다. 텍스트 버퍼는 텍스트 뷰 내에 내용의 현재 상태를 저장 시 사용된다. 텍스트 버퍼는 텍스트, 이미지, 자식 위젯, 텍스트 태그를 비롯해 문서의 렌더링에 필요한 모든 정보를 보유한다.
단일 텍스트 버퍼는 다중 텍스트 뷰에 의해 표시될 수 있지만 각 텍스트 뷰는 하나의 연관된 버퍼만 가진다. 대부분의 프로그래머들은 이러한 기능을 활용하지 않지만 뒷부분에서 자식 위젯을 텍스트 버퍼로 포함(embed)시키는 방법을 학습 시 중요해진다.
GTK+의 모든 텍스트 위젯과 마찬가지로 텍스트는 UTF-8 문자열로 저장된다. UTF-8은 각 문자마다 1 바이트부터 4 바이트까지 이용하는 문자 인코딩 유형이다. 문자가 얼마나 많은 바이트를 차지하는지 구별하기 위해서 "0"은 1 바이트 문자보다 항상 우선하고, "110"는 2 바이트 문자보자 앞서며, "1110"는 3 바이트 시퀀스보다 앞에 오는 식이다. 여러 바이트로 이어지는 UTF-8 문자는 나머지 바이트에서 두 개의 최상위 비트에 "10"을 갖는다.
이를 통해 기본 128 ASCII 문자가 여전히 지원되는데, 처음 "0" 뒤에 단일 바이트의 문자에서 추가 7 비트를 이용할 수 있기 때문이다. UTF-8 또한 다른 여러 언어에서 문자를 지원하기도 한다. 이 방법을 이용하면 더 큰 바이트 시퀀스에서 작은 바이트 시퀀스가 발생하는 일을 피할 수 있다.
텍스트 버퍼를 처리할 때는 두 가지 용어, 오프셋과 색인이라는 용어를 알아야 한다. "오프셋"이란 단어는 하나의 문자를 나타낸다. UTF-8 문자는 버퍼 내부에서 하나 또는 그 이상의 바이트로 이어질 수 있으므로 GtkTextBuffer 내의 문자 오프셋은 단일 바이트가 아닐 수도 있다.
리스팅 7-2는 가장 단순한 형태에 속하는 텍스트 뷰 하나를 생성하는 방법을 보인다. 새로운 GtkTextView 위젯이 생성된다. 위젯의 버퍼가 검색되고 텍스트가 버퍼로 삽입된다. 스크롤이 있는 창을 이용해 텍스트 뷰를 포함시킨다.
리스팅 7-2. 간단한 GtkTextView 위젯 (textview.c)
#include <gtk/gtk.h>
int main (int argc,
char *argv[])
{
GtkWidget *window, *scrolled_win, *textview;
GtkTextBuffer *buffer;
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (window), "Text Views");
gtk_container_set_border_width (GTK_CONTAINER (window), 10);
gtk_widget_set_size_request (window, 250, 150);
textview = gtk_text_view_new ();
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (textview));
gtk_text_buffer_set_text (buffer, "Your 1st GtkTextView widget!", -1);
scrolled_win = gtk_scrolled_window_new (NULL, NULL);
gtk_container_add (GTK_CONTAINER (scrolled_win), textview);
gtk_container_add (GTK_CONTAINER (window), scrolled_win);
gtk_widget_show_all (window);
gtk_main();
return 0;
}
대부분의 새로운 GtkTextView 위젯은 gtk_text_view_new()를 이용해 생성된다. 해당 함수를 이용하면 빈 버퍼가 알아서 생성된다. 이런 기본 버퍼는 후에 gtk_text_view_set_buffer()를 이용해 대체되고 gtk_text_view_get_buffer()를 이용해 검색된다.
초기 버퍼를 개발자가 이미 생성한 버퍼로 설정하고 싶다면 gtK_text_view_new_with_buffer()를 이용해 텍스트 뷰를 생성하면 된다. 대부분의 경우 기본 텍스트 버퍼를 이용하는 편이 수월할 것이다.
GtkTextBuffer 객체로 접근하고 나면 내용을 추가하는 여러 가지 방법이 있지만 gtk_text_buffer_set_text()를 이용하는 방법이 가장 간단하다. 이 함수는 텍스트 버퍼, 버퍼의 새로운 텍스트로 설정하기 위한 UTF-8 텍스트 문자열, 텍스트의 길이를 수신한다.
void gtk_text_buffer_set_text (GtkTextBuffer *buffer,
const gchar *text,
gint length);
텍스트 문자열이 NULL로 끝난다면 문자열 길이에 -1을 사용할 수 있다. 명시된 텍스트 길이보다 널 문자를 먼저 발견한다면 위의 함수는 조용히 실패할 것이다.
버퍼의 현재 내용은 새로운 텍스트 문자열로 완전히 대체될 것이다. "텍스트 반복자와 텍스트 마크" 절에서 현재 내용을 겹쳐 쓰지 않고 텍스트를 버퍼로 삽입할 수 있도록 해주어 대량의 텍스트를 삽입하는 데 적절한 함수들을 소개할 것이다.
앞절의 내용을 상기해보면 네이티브 스크롤링 기능을 가진 위젯은 GtkTextView 위젯을 포함해 다섯 가지가 있었다. 텍스트 뷰에는 조정을 관리하는 기능이 이미 존재하기 때문에 스크롤이 있는 창으로 추가하려면 항상 gtk_container_add()를 이용해야 한다.
텍스트 뷰 프로퍼티
GtkTextView는 다용도의 위젯으로 생성되었다. 그렇게 때문에 해당 위젯에는 많은 프로퍼티가 제공된다. 이번 절에서는 이러한 프로퍼티들 중 다수를 학습할 것이다.
텍스트 뷰 위젯이 그토록 유용한 이유는 개발자가 위젯의 전체 또는 일부에 변경내용을 적용할 수 있다는 점 때문이다. 텍스트 태그를 이용해 텍스트의 세그먼트에 대한 프로퍼티를 변경한다. 문서의 일부만 맞춤설정하는 방법은 후에 다루겠다.
리스팅 7-3은 GtkTextBuffer의 전체 내용을 맞춤설정하는 데 사용 가능한 프로퍼티를 다수 보여준다. 이러한 프로퍼티는 문서의 각 섹션에서 텍스트 태그로 오버라이드될 수 있음을 명심해야 한다.
리스팅 7-3. GtkTextView 프로퍼티 이용하기 (textview2.c)
#include <gtk/gtk.h>
int main (int argc,
char *argv[])
{
GtkWidget *window, *scrolled_win, *textview;
GtkTextBuffer *buffer;
PangoFontDescription *font;
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (window), "Text Views Properties");
gtk_container_set_border_width (GTK_CONTAINER (window), 10);
gtk_widget_set_size_request (window, 250, 150);
font = pango_font_description_from_string ("Monospace Bold 10");
textview = gtk_text_view_new ();
gtk_widget_modify_font (textview, font);
gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (textview), GTK_WRAP_WORD);
gtk_text_view_set_justification (GTK_TEXT_VIEW (textview), GTK_JUSTIFY_RIGHT);
gtk_text_view_set_editable (GTK_TEXT_VIEW (textview), TRUE);
gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW (textview), TRUE);
gtk_text_view_set_pixels_above_lines (GTK_TEXT_VIEW (textview), 5);
gtk_text_view_set_pixels_below_lines (GTK_TEXT_VIEW (textview), 5);
gtk_text_view_set_pixels_inside_wrap (GTK_TEXT_VIEW (textview), 5);
gtk_text_view_set_left_margin (GTK_TEXT_VIEW (textview), 10);
gtk_text_view_set_right_margin (GTK_TEXT_VIEW (textview), 10);
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (textview));
gtk_text_buffer_set_text (buffer, "This is some text!\nChange me!\nPlease!", -1);
scrolled_win = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_win),
GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
gtk_container_add (GTK_CONTAINER (scrolled_win), textview);
gtk_container_add (GTK_CONTAINER (window), scrolled_win);
gtk_widget_show_all (window);
gtk_main();
return 0;
}
GtkTextView의 각 프로퍼티가 무슨 일을 하는지 가장 잘 설명하는 방법은 결과를 보여주는 것이기 때문에 결과의 스크린샷을 그림 7-3에 싣고자 한다. 애플리케이션을 자신의 머신에서 직접 컴파일하고 리스팅 7-3에 사용된 값을 변경하여 어떤 느낌인지 직접 시도해볼 것을 권한다.
텍스트 내용의 일부를 대상으로 글꼴과 색상을 변경하는 것도 가능하지만 리스팅 7-3에서 보인 바와 같이 앞의 여러 장에 실린 함수를 이용해 전체 위젯의 내용을 변경하는 방법도 여전히 이용할 수 있다. 이는 텍스트 파일처럼 스타일이 일관된 문서를 편집할 때 유용하다.
다중 행에 텍스트를 표시하는 위젯을 처리할 때는 텍스트의 래핑 여부와 방식을 결정해야 한다. 리스팅 7-3에서는 gtk_text_view_set_wrap_mode()를 이용해 래핑 모드가 GTK_WRAP_WORD로 설정되었다. 이러한 설정은 텍스트를 래핑하긴 하지만 워드를 2개 이상의 행으로 나누진 않는다. GtkWrapMode 열거에서 이용 가능한 래핑 모드에는 네 가지가 있다.
- GTK_WRAP_NONE: 어떤 래핑도 발생하지 않을 것이다. 스크롤을 이용한 창이 뷰를 포함할 경우 스크롤바가 확장될 것이다. 그 외의 경우 텍스트 뷰가 화면에서 확장될 것이다. 스크롤을 이용한 창이 GtkTExtView 위젯을 포함하지 않으면 위젯은 가로로 확장될 것이다.
- GTK_WRAP_CHAR: 래핑 지점이 워드(word)의 중간에서 발생하더라도 문자까지 래핑한다. 텍스트 에디터의 경우 별로 좋은 선택이 되지 못하는데, 워드를 2개 이상의 행으로 나눌 것이기 때문이다.
- GTK_WRAP_WORD: 행을 가장 큰 수의 워드로 채우지만 래핑을 위해 워드를 나누진 않는다. 대신 전체 워드를 다음 행으로 가져간다.
- GTK_WRAP_WORD_CHAR: GTK_WRAP_WORD와 동일한 방식으로 래핑하지만 표시되는 하나의 텍스트 뷰 너비보다 전체 워드가 더 많은 공간을 차지한다면 문자별로 래핑한다.
때로는 사용자가 문서를 편집하지 못하도록 만들 경우가 있다. editable 프로퍼티는 gtk_text_view_set_editable()을 이용해 전체 텍스트 뷰에서 변경할 수 있다. 이것이 항상 유일한 해답은 아닌데, 텍스트 태그를 이용하면 문서의 특정 섹션에서 이를 오버라이드할 수 있기 때문이다.
이를 대신해 gtk_widget_set_sensitive()를 이용하면 사용자가 위젯과 전혀 상호작용을 하지 못하도록 방지한다. 텍스트 뷰가 편집 불가로 설정된 경우 사용자는 텍스트를 선택하는 것과 같이 텍스트 버퍼의 편집을 요하지 않는 연산을 실행할 수 있을 것이다. 텍스트 뷰를 insensitive로 설정하면 사용자는 이러한 액션을 실행하지 못할 것이다.
문서 내에서 편집을 비활성화할 때는 gtk_text_view_set_cursor_visible()을 이용해 커서의 표시를 중단하는 편이 유용하다. 기본적으로 이 두 개의 프로퍼티는 TRUE로 설정되므로 동시에 똑같이 유지하기 위해서는 둘 다 변경되어야 한다.
기본적으로 행 간에는 여분의 공간이 없지만 리스팅 7-3은 행의 위, 아래, 래핑된 행 사이에 공간을 추가하는 방법을 보여준다. 이러한 함수들은 행들 간에 여분의 공간을 추가하므로 행들 간 충분한 공간이 있을 것으로 간주해도 좋다. 대부분의 경우 이 기능을 사용해선 안 되는데, 사용자에게 이러한 공간이 올바로 보이지 않을 수 있기 때문이다.
양쪽정렬(justification) 또한 텍스트 뷰의 중요한 프로퍼티로, 특히 많은 텍스트 문서를 처리할 때 그러하다. 양쪽정렬의 기본 값은 네 가지로 GTK_JUSTIFY_LEFT, GTK_JUSTIFY_RIGHT, GTK_JUSTIFY_CENTER, GTK_JUSTIFY_FILL이 있다.
양쪽정렬은 gtk_text_view_set_justification()을 이용해 전체 텍스트 뷰를 대상으로 설정할 수 있지만 텍스트 태그를 이용해 텍스트의 특정 섹션에 오버라이드할 수도 있다. 대부분의 경우 사용자가 그것을 변경하고자 하지 않는 한 GTK_JUSTIFY_LEFT 기본 양쪽정렬을 이용할 것이다. 텍스트는 기본적으로 뷰의 좌측으로 정렬된다.
void gtk_text_view_set_justification (GtkTextView *textview,
GtkJustification justification);
리스팅 7-3에서 설정된 마지막 프로퍼티는 왼쪽 여백과 오른쪽 여백이다. 기본적으로는 왼쪽이나 오른쪽에 여분의 여백이 추가되지 않지만 gtk_text_view_set_left_margion()이나 gtk_text_view_set_right_margin()을 이용해 각각 좌측과 우측으로 특정 픽셀만큼 추가하는 방법도 있다.
Pango 탭 배열
텍스트 뷰에 추가된 탭은 기본 너비로 설정되지만 변경하고 싶을 때가 종종 있다. 가령 소스 코드 에디터에서 어떤 사용자는 2개의 공백(space)만큼 들여쓰길 원하는 반면 어떤 사용자는 5개의 공백을 원하는 수도 있다. GTK+는 새로운 탭 크기를 정의하는 PangoTabArray 객체를 제공한다.
기본 탭 크기를 변경할 때는 우선 현재 글꼴을 바탕으로 탭이 차지하게 될 공간의 가로 픽셀 수를 계산해야 한다. 다음으로 make_tab_array() 함수를 이용해 새로운 탭 크기를 계산할 수 있다. 함수는 원하는 공백 수에서 문자열을 생성함으로써 시작한다. 해당 문자열은 이후 PangoLayout 객체로 해석되고, 텍스트 뷰로 적용 가능하다.
static void
make_tab_array (PangoFontDescription *fd,
gsize tab_size,
GtkWidget *textview)
{
PangoTabArray *tab_array;
PangoLayout *layout;
gchar *tab_string;
gint width, height;
g_return_if_fail (tab_size < 100);
tab_string = g_strnfill (tab_size, ' ');
layout = gtk_widget_create_pango_layout (textview, tab_string);
pango_layout_set_font_description (layout, fd);
pango_layout_get_pixel_size (layout, &width, &height);
tab_array = pango_tab_array_new (1, TRUE);
pango_tab_array_set_tab (tab_array, 0, PANGO_TAB_LEFT, width);
gtk_text_view_set_tabs (GTK_TEXT_VIEW (textview), tab_array);
g_free (tab_string);
}
PangoLayout 객체는 텍스트 전체 단락을 나타내는 데 사용된다. 보통 Pango는 위젯 내에서 텍스트를 배치하기 위해 이 객체를 내부적으로 이용한다. 하지만 이 예제에서 탭 문자열의 너비를 계산하는 데 사용할 수 있다.
먼저 GtkTextVew로부터 새로운 PangoLayout 객체를 생성하고 gtk_widget_create_pango_layout()을 이용해 탭 문자열을 생성하겠다. 이는 텍스트 뷰의 기본 글꼴 설명을 이용한다. 전체 문서에 동일한 글꼴이 적용된 경우라면 괜찮다. 텍스트 단락을 렌더링하는 방법을 설명하기 위해 PangoLayout이 사용된다.
PangoLayout* gtk_widget_create_pango_layout (GtkWidget *textview,
const gchar *text);
문서 내 글꼴이 다양하거나 이미 텍스트 뷰로 적용되지 않았다면 계산에 사용할 글꼴을 명시하길 원할 것이다. pango_layout_set_font_description()을 이용하면 Pango 레이아웃의 글꼴을 설정할 수 있다. 이는 레이아웃의 글꼴을 설명하기 위해 PangoFontDescription 객체를 이용한다.
void pango_layout_set_font_description (PangoLayout *layout,
const PangoFontDescription *fd);
자신의 PangoLayout을 올바로 설정하고 나면 pango_layout_get_pixel_size()로 문자열 너비를 검색할 수 있다. 이는 문자열이 버퍼 내에서 차지하게 될 공간을 계산한 값으로, 사용자가 위젯에서 Tab 키를 누르면 추가된다.
void pango_layout_get_pixel_size (PangoLayout *layout,
int *width,
int *height);
탭의 너비를 검색했으니 pango_tab_array_new()를 이용해 새로운 PangoTabArray를 생성할 필요가 있겠다. 이 함수는 배열에 추가되어야 하는 요소의 개수와, 각 요소의 크기가 픽셀로 명시될 것인지 여부를 나타내는 알림(notification)을 수신한다.
void pango_tab_array_new (gint initial_size,
gboolean positions_in_pixels);
이 시점에 지원되는 탭의 타입은 하나밖에 없기 때문에 하나의 요소만 가진 탭 배열을 생성해야 한다. 두 번째 매개변수에 TRUE를 명시하지 않으면 탭은 Pango 단위로서 저장될 것인데, 1 픽셀은 1,024의 Pango 단위와 같다.
탭 배열을 적용하기 전에 우선 너비를 추가해야 한다. 이는 pango_tab_array_set_tab()을 이용해 실행된다. 정수 "0"은 PangoTabArray의 첫 번째 요소, 즉 유일하게 존재해야 하는 요소를 나타낸다. PANGO_TAB_LEFT는 항상 세 번째 매개변수에 대해 명시되어야 하는데, 현재로선 유일하게 지원되는 값이기 때문이다. 마지막 매개변수는 탭의 너비를 픽셀로 나타낸 값이다.
void pango_tab_array_set_tab (PangoTabArray *tabarray,
gint tab_index,
PangoTabAlign alignment,
gint location);
함수로부터 탭 배열을 수신할 때는 gtk_text_view_set_tabs()를 이용해 전체 텍스트 뷰로 적용할 필요가 있겠다. 그래야만 텍스트 뷰 내의 모든 탭이 동일한 너비로 설정되도록 확보할 수 있다. 하지만 다른 모든 텍스트 뷰 프로퍼티와 마찬가지로 텍스트 문단이나 섹션을 대상으로 이 값을 오버라이드할 수 있다.
void gtk_text_view_set_tabs (GtkTextView *textview,
PangoTabArray *tabs);
탭 배열의 이용이 끝나고 더 이상 필요하지 않다면 pango_tab_array_free()를 이용해 해제할 수 있다.
텍스트 반복자와 텍스트 마크
GtkTextBuffer 내에서 텍스트를 조작할 때 버퍼 내에서 위치의 추적을 유지하는 데 사용할 수 있는 객체로 두 가지가 있는데, 바로 GtkTextIter와 GtkTextMark다. 이 두 가지 타입의 객체 간 해석을 위해 GTK+에서는 적절한 함수를 제공한다.
텍스트 반복자는 버퍼 내 두 개의 문자 간 위치를 나타내는 데 사용된다. 이는 버퍼 내에서 텍스트를 조작할 때 활용된다. 텍스트 반복자를 이용 시에는 텍스트 버퍼가 편집되면 자동으로 무효화된다는 문제가 있다. 버퍼에 동일한 문자를 삽입한 후 제거하더라도 텍스트 반복자는 여전히 무효화될 것인데, 이는 반복자가 스택에서 할당되고 즉시 사용되도록 만들어졌기 때문이다.
텍스트 버퍼 내에서 내용이 변경되더라도 위치를 추적하기 위해 GtkTextMark 객체가 제공된다. 텍스트 마크는 버퍼가 조작되는 동안에도 그 상태로 유지되어 버퍼가 조작되는 방식에 따라 이동할 것이다. gtk_text_buffer_get_iter_at_mark()를 이용하면 텍스트 마크를 가리키는 반복자를 검색할 수 있는데, 이는 마크를 문서 내 위치를 추적하기에 가장 적합하도록 만든다.
void gtk_text_buffer_get_iter_at_mark (GtkTextBuffer *buffer,
GtkTextIter *iter,
GtkTextMark *mark);
텍스트 마크는 텍스트가 어떻게 편집되는지에 따라 위치를 변경하여 마치 텍스트 내에 보이지 않는 커서와 같은 역할을 한다. 텍스트가 마크 앞에 추가되면 같은 텍스트 위치에 유지되도록 그 오른쪽으로 이동할 것이다.
기본적으로 텍스트 마크는 오른쪽으로 무게중심(gravity)이 설정된다. 즉, 텍스트가 추가되면서 우측으로 이동할 것이란 의미다. 마크를 둘러싼 텍스트가 삭제된다고 가정해보자. 마크는 삭제된 텍스트의 둘 중 한 쪽에 위치한 두 개의 텍스트 조각 사이의 위치로 이동할 것이다. 이후 오른쪽으로 설정된 무게중심때문에 텍스트가 텍스트 마크로 삽입되면 삽입된 텍스트의 우측에서 계속 남아있을 것이다. 이는 텍스트가 삽입되면 커서가 삽입된 텍스트의 우측에 유지되는 이치와 비슷하다.
기본적으로 텍스트 마크는 텍스트 내에서 표시되지 않는다. 하지만 gtk_text_mark_set_invisible()을 이용하면 텍스트 마크를 표시하도록 설정이 가능한데, 이를 이용 시 위치하는 장소를 나타내기 위해 수직 막대가 표시될 것이다.
텍스트 마크는 두 가지 방식으로 접근이 가능하다. 우선 구체적인 GtkTextIter 위치에서 텍스트 마크를 검색할 수 있다. 그리고 문자열로 된 텍스트 마크를 그 이름으로 설정하여 마크의 추적을 수월하게 만드는 것도 가능하다.
GTK+는 GtkTextBuffer: insert와 selection_bound마다 항상 두 개의 기본 텍스트 마크를 제공한다. insert 텍스트 마크는 버퍼 내 현재 커서의 위치를 나타낸다. 선택된 텍스트가 있을 경우 selection_bound 텍스트 마크는 선택된 텍스트의 경계를 의미한다. 선택된 텍스트가 없다면 이 두 개의 마크는 동일한 위치를 가리킬 것이다.
insert와 selection_bound 텍스트 마크는 버퍼를 조작 시 매우 유용하다. 버퍼 내에서 텍스트를 자동으로 선택하거나 선택해제하고 버퍼 내에서 논리적으로 삽입되어야 하는 텍스트의 위치를 알아내는 데 도움이 되도록 조작할 수 있다.
텍스트 버퍼 편집하기
GTK+는 텍스트 버퍼를 조작하는 것 뿐만 아니라 텍스트 반복자를 검색하기 위한 함수들도 광범위하게 제공한다. 이번 절에서는 리스팅 7-4에 사용된 방법들 중 중요한 사항만 몇 가지 살펴볼 것이며 나머지는 추후 소개하도록 하겠다. 그림 7-4는 GtkTextBuffer를 이용해 텍스트를 삽입 및 검색하는 애플리케이션을 표시한 것이다.
리스팅 7-4는 두 개의 함수를 실행하는 간단한 예제에 속한다. 그림 7-4에 표시된 Insert Text 버튼을 클릭하면 GtkEntry 위젯에 표시되는 문자열이 현재 커서 위치로 삽입된다. Get Text 버튼을 클릭하면 어떤 선택된 문자든 g_print()를 이용해 출력된다.
리스팅 7-4. 텍스트 반복자 이용하기 (iterators.c)
#include <gtk/gtk.h>
typedef struct
{
GtkWidget *entry, *textview;
} Widgets;
static void insert_text (GtkButton*, Widgets*);
static void retrieve_text (GtkButton*, Widgets*);
int main (int argc,
char *argv[])
{
GtkWidget *window, *scrolled_win, *hbox, *vbox, *insert, *retrieve;
Widgets *w = g_slice_new (Widgets);
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (window), "Text Iterators");
gtk_container_set_border_width (GTK_CONTAINER (window), 10);
gtk_widget_set_size_request (window, -1, 200);
w->textview = gtk_text_view_new ();
w->entry = gtk_entry_new ();
insert = gtk_button_new_with_label ("Insert Text");
retrieve = gtk_button_new_with_label ("Get Text");
g_signal_connect (G_OBJECT (insert), "clicked",
G_CALLBACK (insert_text),
(gpointer) w);
g_signal_connect (G_OBJECT (retrieve), "clicked",
G_CALLBACK (retrieve_text),
(gpointer) w);
scrolled_win = gtk_scrolled_window_new (NULL, NULL);
gtk_container_add (GTK_CONTAINER (scrolled_win), w->textview);
hbox = gtk_hbox_new (FALSE, 5);
gtk_box_pack_start_defaults (GTK_BOX (hbox), w->entry);
gtk_box_pack_start_defaults (GTK_BOX (hbox), insert);
gtk_box_pack_start_defaults (GTK_BOX (hbox), retrieve);
vbox = gtk_vbox_new (FALSE, 5);
gtk_box_pack_start (GTK_BOX (vbox), scrolled_win, TRUE, TRUE, 0);
gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, TRUE, 0);
gtk_container_add (GTK_CONTAINER (window), vbox);
gtk_widget_show_all (window);
gtk_main();
return 0;
}
/* Insert the text from the GtkEntry into the GtkTextView. */
static void
insert_text (GtkButton *button,
Widgets *w)
{
GtkTextBuffer *buffer;
GtkTextMark *mark;
GtkTextIter iter;
const gchar *text;
buffer = gtk_text_view_get_buffer
(GTK_TEXT_VIEW (w->textview));
text = gtk_entry_get_text (GTK_ENTRY (w->entry));
mark = gtk_text_buffer_get_insert (buffer);
gtk_text_buffer_get_iter_at_mark (buffer, &iter, mark);
gtk_text_buffer_insert (buffer, &iter, text, -1);
}
/* Retrieve the selected text from the GtkTextView and display it
* to the user. */
static void
retrieve_text (GtkButton *button,
Widgets *w)
{
GtkTextBuffer *buffer;
GtkTextIter start, end;
gchar *text;
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (w->textview));
gtk_text_buffer_get_selection_bounds (buffer, &start, &end);
text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
g_print ("%s\n", text);
}
리스팅 7-4를 보면 GTK+의 대부분 객체와 달리 텍스트 반복자는 포인터가 아닌 객체로 저장된다. 즉, 스택에 직접 할당된다는 뜻이다. 반복자를 가리키는 포인터는 이후 주소 연산자를 이용해 함수로 전달된다.
동일한 반복자를 계속 반복하여 사용할 수 있다는 점은 또 다른 중요한 프로퍼티가 되는데, 반복자는 개발자가 텍스트 버퍼를 편집할 때마다 무효화되기 때문이다. 이를 통해 개발자는 엄청난 수의 변수를 생성하는 대신 동일한 GtkTextIter 객체를 재사용할 수 있는 것이다.
텍스트 반복자와 텍스트 마크 검색하기
앞서 언급하였듯 텍스트 반복자와 텍스트 마크를 검색하는 데 이용할 수 있는 함수의 수는 꽤 많으며, 그 중 다수는 이번 장에 걸쳐 사용될 것이다.
리스팅 7-4에서는 gtk_text_buffer_get_insert()를 이용해 insert 마크를 검색하면서 시작된다. gtk_text_buffer_get_selection_boud()를 이용해 selection_bound 텍스트 마크를 검색하는 방법도 있다.
mark = gtk_text_buffer_get_insert (buffer);
gtk_text_buffer_get_iter_at_mark (buffer, &iter, mark);
텍스트 마크를 검색했다면 gtk_text_buffer_get_iter_at_mark()를 이용해 텍스트 반복자로 해석하여 버퍼를 조작하는 데 사용할 수 있다.
리스팅 7-4에서 텍스트 반복자를 검색하는 데 사용된 또 다른 함수는 gtk_text_buffer_get_selection_bounds()인데, 이는 insert 와 selection_bound 마크에 위치한 반복자를 리턴한다. 텍스트 반복자 매개변수 두 개 중 하나 또는 둘 다를 NULL로 설정하면 값이 리턴되지 않으며, 둘 중 하나만 필요하다면 특정 마크에 대한 함수를 사용하는 것이 맞겠다.
버퍼의 내용을 검색할 때는 텍스트의 슬라이스에 대한 시작 및 끝 반복자를 명시해야 할 필요가 있다. 문서의 전체 내용을 얻고자 한다면 문서의 시작과 끝을 가리키는 반복자가 필요한데, 이는 gtk_text_buffer_get_bounds()를 이용해 검색할 수 있다.
void gtk_text_buffer_get_bounds (GtkTextBuffer *buffer,
GtkTextIter *start,
GtkTextIter *end);
gtk_text_buffer_get_start_iter()이나 gtk_text_buffer_get_end_iter()를 이용하면 다른 부분과 무관하게 텍스트 버퍼의 시작 반복자 혹은 끝 반복자만 검색할 수도 있다.
버퍼 내의 텍스트는 gtk_text_buffer_get_text()를 이용해 검색 가능하다. 이는 start 반복자와 end 반복자 사이에 위치한 모든 텍스트를 리턴한다. 마지막 매개변수가 TRUE로 설정되면 표시되지 않은(invisible) 텍스트도 리턴될 것이다.
gchar* gtk_text_buffer_get_text (GtkTextBuffer *buffer,
const GtkTextIter *start,
const GtkTextIter *end,
gboolean include_hidden_chars);
버퍼의 전체 내용을 검색할 때는 gtk_text_buffer_get_text()만 사용해야 한다. 이는 텍스트 버퍼 내에 내장된 이미지나 위젯 객체는 모두 무시하므로 문자 색인이 올바른 위치에 해당하지 않을지도 모른다. 텍스트 버퍼의 일부분만 검색하려면 gtk_text_buffer_get_slice()를 대신 사용하라.
오프셋은 버퍼 내 각 문자의 개수를 나타낸다는 사실을 상기해보자. 이러한 문자들의 길이는 1 또는 그 이상의 바이트가 될 수 있다. gtk_text_buffer_get_iter_at_offset() 함수는 버퍼의 시작부터 특정 오프셋 위치에서 반복자를 검색하도록 해준다.
void gtk_text_buffer_get_iter_at_offset (GtkTextBuffer *buffer,
GtkTextIter *iter,
gint character_offset);
GTK+는 그 외에도 gtk_text_buffer_get_iter_at_line_index()를 제공하여 명시된 행에서 각 바이트의 위치를 선택할 수 있게 해준다. 이 함수를 사용 시에는 각별히 주의를 기울여야 하는데, 색인은 항시 UTF-8 문자의 시작을 가리켜야 하기 때문이다. UTF-8의 문자는 단일 바이트 이상일 수도 있음을 기억하라!
문자 오프셋을 선택하는 대신 gtk_text_buffer_get_iter_at_line()을 이용해 명시된 행에서 첫 번째 반복자를 검색하는 수도 있다.
void gtk_text_buffer_get_iter_at_line (GtkTextBuffer *buffer,
GtkTextIter *iter,
gint character_offset);
명시된 행의 첫 번째 문자로부터 오프셋에 위치한 반복자를 검색하고 싶다면 gtk_text_buffer_get_iter_at_line_offset()을 이용하면 된다.
텍스트 버퍼 내용 변경하기
전체 텍스트 버퍼의 내용을 리셋하는 방법은 이미 학습했지만 문서의 일부를 편집하는 방법도 익힌다면 유용하겠다. 이러한 목적으로 수많은 함수가 제공된다. 리스팅 7-4는 텍스트를 버퍼로 삽입하는 방법을 보여준다.
버퍼의 임의 위치로 텍스트를 삽입해야 한다면 gtk_text_buffer_insert()를 이용해야 한다. 이를 실행하기 위해선 삽입지점을 가리키는 GtkTextIter, 버퍼로 삽입해야 하는 UTF-8로 된 텍스트 문자열, 그리고 텍스트의 길이가 필요하다. 텍스트 문자열이 NULL로 끝난다면 길이는 -1로 명시할 수 있다.
GtkTextMark* gtk_text_buffer_get_insert (GtkTextBuffer *buffer);
위의 함수가 호출되면 텍스트 버퍼는 insert-text 시그널을 발생시킬 것이며, 텍스트 반복자는 무효화될 것이다. 하지만 텍스트 반복자는 삽입된 텍스트의 끝으로 재초기화(reinitialized)될 것이다.
gtk_text_buffer_insert_at_cursor()라는 편의 함수를 이용하면 커서의 현재 위치에서 gtk_text_buffer_insert()를 호출할 수 있다. 이는 insert 텍스트 마크를 이용해 쉽게 구현이 가능하지만, 반복적인 호출을 피하도록 도와준다.
void gtk_text_buffer_insert_at_cursor (GtkTextBuffer *buffer,
const gchar *text,
gint length);
gtk_text_buffer_delete()를 이용하면 두 개의 텍스트 반복자 사이의 텍스트를 제거할 수 있다. 개발자가 반복자를 명시하는 순서는 무관한데, 함수가 자동으로 올바른 순서대로 위치시키기 때문이다.
void gtk_text_buffer_delete (GtkTextBuffer *buffer,
GtkTextIter *start,
GtkTextIter *end);
위의 함수는 delete-range 시그널을 발생시켜 두 개의 반복자를 모두 무효화할 것이다. 하지만 시작 반복자와 끝 반복자를 제거된 텍스트의 시작 위치로 재초기화할 것이다.
텍스트 자르기, 복사하기, 붙여넣기
GtkTextView 위젯을 오른쪽 마우스로 클릭하면 여러 개의 옵션을 포함하는 팝업 메뉴가 뜬다. 해당 메뉴의 예제를 그림 7-5에 실었는데, 그 내용은 시스템에 따라 다를 수 있다.
이 옵션들 중 자르기(cut), 복사하기(copy), 붙여넣기(paste)는 대부분 모든 텍스트 에디터에서 표준 옵션에 해당한다. 이들은 모든 GtkTextView 위젯에 빌드된다. 하지만 애플리케이션 메뉴나 툴바에서 이 함수들에 대해 자신만의 버전을 구현하길 원할 때가 있다.
리스팅 7-5는 이 방법을 각각 제시한다. 세 개의 GtkButton 위젯 중 하나를 클릭하면 어떤 액션이 초기화된다. 버튼과 오른쪽 마우스의 클릭 메뉴를 이용하면 둘 다 동일한 GtkClipboard 객체를 사용함을 확인할 수 있다. 이러한 함수들은 내장된 키보드 가속기, 각각 Ctrl+C, Ctrl+X, Ctrl+V를 이용해 호출할 수도 있다.
리스팅 7-5. Cut, Copy, Paste 연산 이용하기 (cutcopypaste.c)
#include <gtk/gtk.h>
static void cut_clicked (GtkButton*, GtkTextView*);
static void copy_clicked (GtkButton*, GtkTextView*);
static void paste_clicked (GtkButton*, GtkTextView*);
int main (int argc,
char *argv[])
{
GtkWidget *window, *scrolled_win, *textview, *cut, *copy, *paste, *hbox, *vbox;
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (window), "Cut, Copy & Paste");
gtk_container_set_border_width (GTK_CONTAINER (window), 10);
textview = gtk_text_view_new ();
cut = gtk_button_new_from_stock (GTK_STOCK_CUT);
copy = gtk_button_new_from_stock (GTK_STOCK_COPY);
paste = gtk_button_new_from_stock (GTK_STOCK_PASTE);
g_signal_connect (G_OBJECT (cut), "clicked",
G_CALLBACK (cut_clicked),
(gpointer) textview);
g_signal_connect (G_OBJECT (copy), "clicked",
G_CALLBACK (copy_clicked),
(gpointer) textview);
g_signal_connect (G_OBJECT (paste), "clicked",
G_CALLBACK (paste_clicked),
(gpointer) textview);
scrolled_win = gtk_scrolled_window_new (NULL, NULL);
gtk_widget_set_size_request (scrolled_win, 300, 200);
gtk_container_add (GTK_CONTAINER (scrolled_win), textview);
hbox = gtk_hbox_new (TRUE, 5);
gtk_box_pack_start (GTK_BOX (hbox), cut, TRUE, TRUE, 0);
gtk_box_pack_start (GTK_BOX (hbox), copy, TRUE, TRUE, 0);
gtk_box_pack_start (GTK_BOX (hbox), paste, TRUE, TRUE, 0);
vbox = gtk_vbox_new (FALSE, 5);
gtk_box_pack_start (GTK_BOX (vbox), scrolled_win, TRUE, TRUE, 0);
gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, TRUE, 0);
gtk_container_add (GTK_CONTAINER (window), vbox);
gtk_widget_show_all (window);
gtk_main();
return 0;
}
/* Copy the selected text to the clipboard and remove it from the buffer. */
static void
cut_clicked (GtkButton *cut,
GtkTextView *textview)
{
GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
GtkTextBuffer *buffer = gtk_text_view_get_buffer (textview);
gtk_text_buffer_cut_clipboard (buffer, clipboard, TRUE);
}
/* Copy the selected text to the clipboard. */
static void
copy_clicked (GtkButton *copy,
GtkTextView *textview)
{
GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
GtkTextBuffer *buffer = gtk_text_view_get_buffer (textview);
gtk_text_buffer_copy_clipboard (buffer, clipboard);
}
/* Insert the text from the clipboard into the text buffer. */
static void
paste_clicked (GtkButton *paste,
GtkTextView *textview)
{
GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
GtkTextBuffer *buffer = gtk_text_view_get_buffer (textview);
gtk_text_buffer_paste_clipboard (buffer, clipboard, NULL, TRUE);
}
GtkClipboard는 애플리케이션 간에 쉽게 데이터를 전송할 수 있는 중앙(central) 클래스다. 이미 생성된 클립보드를 검색하려면 gtk_clipboard_get()를 이용해야 한다. 기본 클립보드가 제공되기 때문에 본문에서는 자신만의 클립보드 객체를 생성하는 방법은 알려주지 않겠다.
자신만의 GtkClipboard 객체를 생성하는 것이 가능하지만 기본 작업을 실행할 때는 기본 클립보드를 이용해야 한다. 이는 GDK_SELECTION_CLIPBOARD를 gtk_clipboard_get()로 전달함으로써 검색이 가능하다.
자신이 이미 생성한 GtkClipboard 객체로 데이터를 추가하거나 제거하는 등 직접 상호작용을 실행하는 것이 가능하다. 하지만 GtkTextView 위젯에 대한 텍스트 문자열을 복사하거나 검색하는 일처럼 간단한 작업을 실행할 때는 GtkTextBuffer의 내장된 함수를 이용하는 편이 더 옳다.
GtkTextBuffer의 세 가지 클립보드 액션 중 가장 간단한 액션은 텍스트의 복사로, 아래를 이용해 실행하면 된다.
void gtk_text_buffer_copy_clipboard (GtkTextBuffer *buffer,
GtkClipboard *clipboard);
두 번째 클립보드 함수 gtk_text_buffer_cut_clipboard()는 선택내용을 클립보드로 복사할 뿐만 아니라 버퍼로부터 이를 제거하기도 한다. 선택된 텍스트가 편집 가능한 플래그 집합을 갖고 있지 않은 경우 함수의 세 번째 함수로 설정될 것이다. 이 함수는 텍스트 뿐만 아니라 이미지와 텍스트 태그와 같은 내장된(embedded) 객체들도 복사할 것이다.
void gtk_text_buffer_cut_clipboard (GtkTextBuffer *buffer,
GtkClipboard *clipboard,
gboolean default_editable);
마지막 클립보드 함수인 gtk_text_buffer_paste_clipboard()는 먼저 클립보드 내용을 검색한다. 그 다음 함수는 두 가지 일 중 하나를 실행할 것이다. GtkTextIter를 수락하는 세 번째 매개변수가 명시되었다면 내용은 해당 반복자의 지점으로 삽입될 것이다. 세 번째 매개변수에 NULL을 명시하였다면 내용은 커서에 삽입될 것이다.
void gtk_text_buffer_paste_clipboard (GtkTextBuffer *buffer,
GtkClipboard *clipboard,
GtkTextIter *override_location,
gboolean default_editable);
붙여 넣을 내용 중 편집 가능한 플래그 집합을 갖고 있지 않다면 자동으로 default_editable로 설정될 것이다. 대부분의 경우 해당 매개변수를 TRUE로 설정해야 하는데, 그래야만 붙여 넣은 내용의 편집이 가능하기 때문이다. 붙여넣기 연산은 비동기식으로 이루어진다는 사실도 명심한다.
텍스트 버퍼 검색하기
GtkTextView 위젯을 이용하는 대부분 애플리케이션에서는 하나 또는 그 이상의 인스턴스에서 텍스트 버퍼를 검색할 필요가 있다. GTK+는 버퍼에서 텍스트를 검색하도록 gtk_text_iter_forward_search()와 gtk_text_iter_backward_search(), 두 개의 함수를 제공한다.
그림 7-6에 실린 스크린샷은 gtk_text_iter_forward_search()를 이용해 GtkTextBuffer에서 텍스트 문자열을 검색하는 방법을 보인 것이다. 예제는 사용자가 GTK_STOCK_FIND 버튼을 클릭하면서 시작된다.
리스팅 7-6의 애플리케이션은 텍스트 버퍼 내에서 명시된 문자열의 모든 인스턴스를 검색한다. 문자열이 문서에서 몇 번 발견되었는지를 표시하는 대화상자가 사용자에게 표시될 것이다.
리스팅 7-6. GtkTextIter 검색 함수 이용하기 (find.c)
#include <gtk/gtk.h>
typedef struct
{
GtkWidget *entry, *textview;
} Widgets;
static void search (GtkButton*, Widgets*);
int main (int argc,
char *argv[])
{
GtkWidget *window, *scrolled_win, *vbox, *hbox, *find;
Widgets *w = g_slice_new (Widgets);
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (window), "Searching Buffers");
gtk_container_set_border_width (GTK_CONTAINER (window), 10);
w->textview = gtk_text_view_new ();
w->entry = gtk_entry_new ();
gtk_entry_set_text (GTK_ENTRY (w->entry), "Search for ...");
find = gtk_button_new_from_stock (GTK_STOCK_FIND);
g_signal_connect (G_OBJECT (find), "clicked",
G_CALLBACK (search),
(gpointer) w);
scrolled_win = gtk_scrolled_window_new (NULL, NULL);
gtk_widget_set_size_request (scrolled_win, 250, 200);
gtk_container_add (GTK_CONTAINER (scrolled_win), w->textview);
hbox = gtk_hbox_new (FALSE, 5);
gtk_box_pack_start (GTK_BOX (hbox), w->entry, TRUE, TRUE, 0);
gtk_box_pack_start (GTK_BOX (hbox), find, FALSE, TRUE, 0);
vbox = gtk_vbox_new (FALSE, 5);
gtk_box_pack_start (GTK_BOX (vbox), scrolled_win, TRUE, TRUE, 0);
gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, TRUE, 0);
gtk_container_add (GTK_CONTAINER (window), vbox);
gtk_widget_show_all (window);
gtk_main();
return 0;
}
/* Search for the entered string within the GtkTextView. Then tell the user
* how many times it was found. */
static void
search (GtkButton *button,
Widgets *w)
{
const gchar *find;
gchar *output;
GtkWidget *dialog;
GtkTextBuffer *buffer;
GtkTextIter start, begin, end;
gboolean success;
gint i = 0;
find = gtk_entry_get_text (GTK_ENTRY (w->entry));
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (w->textview));
gtk_text_buffer_get_start_iter (buffer, &start);
success = gtk_text_iter_forward_search (&start, (gchar*) find, 0,
&begin, &end, NULL);
while (success)
{
gtk_text_iter_forward_char (&start);
success = gtk_text_iter_forward_search (&start, (gchar*) find, 0,
&begin, &end, NULL);
start = begin;
i++;
}
output = g_strdup_printf ("The string '%s' was found %i times!", find, i);
dialog = gtk_message_dialog_new (NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_INFO,
GTK_BUTTONS_OK, output, NULL);
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
g_free (output);
}
검색 함수가 가장 먼저 하는 일은 gtk_text_buffer_get_start_iter()를 이용해 문서의 하위 검색 범위(lower search bound)를 검색하는 일이다. 검색의 범위를 정하지 않은 채로 두면 문서의 끝을 한계로 자동 설정하기 때문에 버퍼의 바운딩(bounding) 위치가 필요 없다.
전방향 검색은 gtk_text_iter_forward_search()를 통해 실행되는데, 텍스트가 발견되면 TRUE가 리턴된다. 그 외의 경우 FALSE를 리턴한다.
success = gtk_text_iter_forward_search (&start, find, 0, &begin, &end, NULL);
개발자는 가장 먼저 시작 위치 반복자를 명시해야 한다. 그러면 명시된 위치 이후의 텍스트만 검색될 것이다. 다음으로, 검색해야 할 텍스트를 명시한다. 세 번째 매개변수는 개발자가 원할 경우 GtkTextSearchFlags 열거를 명시하도록 해주는데, 열거 값은 아래와 같이 구성된다.
- GTK_TEXT_SEARCH_VISIBLE_ONLY: 버퍼에 숨겨진 요소는 검색하지 않는다.
- GTK_TEXT_SEARCH_TEXT_ONLY: 검색 시 이미지, 자식 위젯, 또는 텍스트가 아닌 객체의 유형은 모두 무시한다.
GTK_TEXT_SEARCH_TEXT_ONLY 플래그를 명시하지 않으면 각 자식 위젯과 내장된 pixbufs를 표현하는 데에 특수 oxFFFC 문자를 이용해야 한다. 매치는 정확해야 하므로 플래그가 있는 비텍스트(nontextual) 요소는 무시하는 것이 좋은 생각이겠다. 기본적으로 모든 검색은 대·소문자를 구별하여 이루어지는데, 플래그는 향후 대·소문자를 구별하지 않는 검색을 지원 시 도입될지도 모른다.
다음 두 가지 반복자는 매치가 발견될 경우 첫 번째 매치의 시작 및 끝 위치를 명시한다. 매치 위치의 추적을 원하지 않는다면 두 반복자에 대해 모두 NULL을 명시하면 된다.
마지막 매개변수는 검색에 대한 바운딩 반복자를 명시하도록 해준다. 함수는 매치의 한계까지만 검색할 것이다. 프로그램이 큰 버퍼를 다루어야 한다면 검색을 제한하는 것이 좋다. 제한하지 않을 경우 검색이 완료될 때까지 화면 잠금이 발생할 위험이 있다. 버퍼의 끝까지만 검색을 원한다면 바운딩 반복자로 NULL을 설정하라.
gtk_text_iter_backward_search()를 이용한 검색은 gtk_text_iter_forward_search()와 동일한 방식으로 작동하는데, limit가 start_pos 이전에 발생해야 한다는 점에서 차이가 있다. 반복자의 한계를 설정하지 않으면 함수는 버퍼의 끝을 한계로 가정할 것이다. 이럴 경우 전체 버퍼를 반복적으로 검색하거나 큰 버퍼를 검색 시 시간이 어느 정도 소요될 수 있으므로 주의해야 한다.
gboolean gtk_text_iter_backward_search (const GtkTextIter *start_pos,
const gchar *text_string,
GtkTextSearchFlags flags,
GtkTextIter *match_start,
GtkTextIter *match_end,
const GtkTextIter *limit);
대부분 애플리케이션에서 검색할 때는 매치를 선택함으로써 표시하길 원할 것이다. 이는 gtk_text_buffer_select_range()를 이용하면 된다. 해당 함수는 insert와 selection_bound 마크를 동시에 두 반복자 위치로 이동시킨다.
void gtk_text_buffer_select_range (GtkTextBuffer *buffer,
const GtkTextIter *ins,
const GtkTextIter *sel_bound);
마크를 두 단계에 거쳐 수동으로 이동시키면 선택한 텍스트가 여러 번 변경되면서 화면이 약간 흔들릴 것이다. 위의 함수는 선택내용을 강제로 한 번만 재계산되도록 하여 이러한 혼동을 피한다.
텍스트 버퍼 스크롤하기
GTK+는 개발자가 선택한 검색 매치를 자동으로 스크롤하지 않는다. 이를 수행하려면 gtk_text_buffer_create_mark()를 이용해 발견된 텍스트 위치에 임의의 GtkTextMark를 생성해야 한다.
GtkTextMark* gtk_text_buffer_create_mark (GtkTextBuffer *buffer,
const gchar *name,
const GtkTextIter *location,
gboolean left_gravity);
gtk_text_buffer_create_mark()의 두 번째 매개변수는 텍스트 문자열을 마크에 대한 이름으로 명시하도록 해준다. 이 이름은 후에 실제 마크 객체 없이 마크를 참조하는 데 사용할 수 있다. 마크는 명시된 텍스트 반복자 위치에 생성된다. 마지막 매개변수를 TRUE로 설정하면 좌측에 무게중심(left gravity)을 둔 마크가 생성될 것이다.
그리고 gtk_text_view_scroll_mark_onscreen()을 이용해 버퍼를 스크롤하면 마크가 화면에 위치한다. 마크의 사용이 끝나면 gtk_text_buffer_delete_mark()를 이용해 버퍼에서 마크를 제거할 수 있다.
void gtk_text_view_scroll_mark_onscreen (GtkTextView *textview,
GtkTextMark *mark);
gtk_text_view_scroll_mark_onscreen()을 이용 시 문제는 화면에 마크를 표시하는 데 최소한의 거리만 스크롤한다는 점이다. 가령 버퍼 내에서 마크를 중앙으로 정렬한다고 치자. 시각적 버퍼에서 마크가 표시되는 위치에 대한 정렬 매개변수를 명시하기 위해선 gtk_text_view_scroll_to_mark()를 호출한다.
void gtk_text_view_scroll_to_mark (GtkTextView *textview,
GtkTextMark *mark,
gdouble margin,
gboolean use_align,
gdouble xalign,
gdouble yalign);
가장 먼저 여백(margin)을 위치시키면 스크롤 가능한 영역이 줄어들 것이다. 여백은 부동 소수점 수로 명시되어야만 영역을 그만큼 감소시킬 수 있다. 대부분의 경우 영역이 전혀 줄어들지 않도록 여백을 0.0으로 사용하길 원할 것이다.
use_align 매개변수에 FALSE를 명시하면 함수는 화면에 마크를 표시하기 위해 최소 거리만큼 스크롤할 것이다. 그 외의 경우 함수는 두 개의 정렬 매개변수를 지표로 이용하여 시각적 영역 내에서 마크의 수평 및 수직 정렬을 명시하도록 해준다.
정렬이 0.0인 경우 시각적 영역의 좌측이나 상단을 의미하고, 1.0은 우측이나 하단, 0.5는 중앙을 의미한다. 함수는 가능한 한 멀리 스크롤되겠지만 명시된 위치로 마크를 스크롤할 수는 없을 것이다. 예를 들어, 버퍼가 하나의 문자 높이(character tall)보다 크다면 버퍼에서 마지막 행을 상단까지 스크롤하기란 불가능하다.
gtk_text_view_scroll_to_iter()라는 함수도 존재하는데, 이는 gtk_text_view_scroll_to_mark()와 동일한 방식으로 행동한다. 유일한 차이점은 위치에 GtkTextMark 대신 GtkTextIter를 수신한다는 사실이지만, 대부분의 경우 텍스트 마크를 사용해야 한다.
텍스트 태그
여태까지는 GtkTextBuffer에서 모든 텍스트의 프로퍼티를 변경하기 위해 제공되는 많은 함수들을 살펴보았다. 하지만 앞서 언급했듯이 GtkTextTag 객체를 이용하면 텍스트의 각 섹션에 대한 디스플레이 프로퍼티를 변경하는 것도 가능하다.
텍스트 태그는 텍스트의 부분마다 텍스트 스타일이 다른 문서를 생성하도록 해주는데, 이를 보통 서식이 있는(rich) 텍스트 편집이라 부른다. 여러 개의 텍스트 스타일을 이용하는 GtkTextView의 스크린샷을 그림 7-7에 실었다.
텍스트 태그는 사실상 적용하기 가장 간단한 개념이다. 리스팅 7-7에서는 사용자가 여러 개의 스타일을 적용하거나 선택내용에서 모든 태그를 제거할 수 있도록 해주는 애플리케이션을 생성한다. 나머지 절을 읽고 나면 리스팅 7-7을 수정하여 여러 스타일 옵션을 추가시킴으로써 다른 텍스트 프로퍼티도 시도해보길 권한다.
리스팅 7-7. 텍스트 태그 이용하기 (texttags.c)
#include <gtk/gtk.h>
typedef struct
{
gchar *str;
double scale;
} text_to_double;
const text_to_double text_scales[] =
{
{ "Quarter Sized", (double) 0.25 },
{ "Double Extra Small", PANGO_SCALE_XX_SMALL},
{ "Extra Small", PANGO_SCALE_X_SMALL},
{ "Small", PANGO_SCALE_SMALL },
{ "Medium", PANGO_SCALE_MEDIUM },
{ "Large", PANGO_SCALE_LARGE},
{ "Extra Large", PANGO_SCALE_X_LARGE},
{ "Double Extra Large", PANGO_SCALE_XX_LARGE},
{ "Double Sized", (double) 2.0 },
{ NULL, 0 }
};
static void format (GtkWidget*, GtkTextView*);
static void scale_changed (GtkComboBox*, GtkTextView*);
static void clear_clicked (GtkButton*, GtkTextView*);
int main (int argc,
char *argv[])
{
GtkWidget *window, *scrolled_win, *textview, *hbox, *vbox;
GtkWidget *bold, *italic, *underline, *strike, *scale, *clear;
GtkTextBuffer *buffer;
gint i = 0;
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (window), "Text Tags");
gtk_container_set_border_width (GTK_CONTAINER (window), 10);
gtk_widget_set_size_request (window, 500, -1);
textview = gtk_text_view_new ();
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (textview));
gtk_text_buffer_create_tag (buffer, "bold", "weight", PANGO_WEIGHT_BOLD, NULL);
gtk_text_buffer_create_tag (buffer,
"italic", "style", PANGO_STYLE_ITALIC, NULL);
gtk_text_buffer_create_tag (buffer, "strike", "strikethrough", TRUE, NULL);
gtk_text_buffer_create_tag (buffer, "underline", "underline",
PANGO_UNDERLINE_SINGLE, NULL);
bold = gtk_button_new_from_stock (GTK_STOCK_BOLD);
italic = gtk_button_new_from_stock (GTK_STOCK_ITALIC);
underline = gtk_button_new_from_stock (GTK_STOCK_UNDERLINE);
strike = gtk_button_new_from_stock (GTK_STOCK_STRIKETHROUGH);
clear = gtk_button_new_from_stock (GTK_STOCK_CLEAR);
scale = gtk_combo_box_new_text();
/* Add choices to the GtkComboBox widget. */
for (i = 0; text_scales[i].str != NULL; i++)
{
gtk_combo_box_append_text (GTK_COMBO_BOX (scale), text_scales[i].str);
gtk_text_buffer_create_tag (buffer, text_scales[i].str, "scale",
text_scales[i].scale, NULL );
}
/* Add the name of the text tag as a data parameter of the object. */
g_object_set_data (G_OBJECT (bold), "tag", "bold");
g_object_set_data (G_OBJECT (italic), "tag", "italic");
g_object_set_data (G_OBJECT (underline), "tag", "underline");
g_object_set_data (G_OBJECT (strike), "tag", "strike");
/* Connect each of the buttons and the combo box to the necessary signals. */
g_signal_connect (G_OBJECT (bold), "clicked",
G_CALLBACK (format), (gpointer) textview);
g_signal_connect (G_OBJECT (italic), "clicked",
G_CALLBACK (format), (gpointer) textview);
g_signal_connect (G_OBJECT (underline), "clicked",
G_CALLBACK (format), (gpointer) textview);
g_signal_connect (G_OBJECT (strike), "clicked",
G_CALLBACK (format), (gpointer) textview);
g_signal_connect (G_OBJECT (scale), "changed",
G_CALLBACK (scale_changed),
(gpointer) textview);
g_signal_connect (G_OBJECT (clear), "clicked",
G_CALLBACK (clear_clicked),
(gpointer) textview);
/* Pack the widgets into a GtkVBox, GtkHBox, and then into the window. */
vbox = gtk_vbox_new (TRUE, 5);
gtk_box_pack_start (GTK_BOX (vbox), bold, FALSE, FALSE, 0);
gtk_box_pack_start (GTK_BOX (vbox), italic, FALSE, FALSE, 0);
gtk_box_pack_start (GTK_BOX (vbox), underline, FALSE, FALSE, 0);
gtk_box_pack_start (GTK_BOX (vbox), strike, FALSE, FALSE, 0);
gtk_box_pack_start (GTK_BOX (vbox), scale, FALSE, FALSE, 0);
gtk_box_pack_start (GTK_BOX (vbox), clear, FALSE, FALSE, 0);
scrolled_win = gtk_scrolled_window_new (NULL, NULL);
gtk_container_add (GTK_CONTAINER (scrolled_win), textview);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_win),
GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
hbox = gtk_hbox_new (FALSE, 5);
gtk_box_pack_start (GTK_BOX (hbox), scrolled_win, TRUE, TRUE, 0);
gtk_box_pack_start (GTK_BOX (hbox), vbox, FALSE, TRUE, 0);
gtk_container_add (GTK_CONTAINER (window), hbox);
gtk_widget_show_all (window);
gtk_main();
return 0;
}
/* Retrieve the tag from the "tag" object data and apply it to the selection. */
static void
format (GtkWidget *widget,
GtkTextView *textview)
{
GtkTextIter start, end;
GtkTextBuffer *buffer;
gchar *tagname;
tagname = (gchar*) g_object_get_data (G_OBJECT (widget), "tag");
buffer = gtk_text_view_get_buffer (textview);
gtk_text_buffer_get_selection_bounds (buffer, &start, &end);
gtk_text_buffer_apply_tag_by_name (buffer, tagname, &start, &end);
}
/* Apply the selected text size property as the tag. */
static void
scale_changed (GtkComboBox *combo,
GtkTextView *textview)
{
const gchar *text;
if (gtk_combo_box_get_active (combo) == -1)
return;
text = gtk_combo_box_get_active_text (combo);
g_object_set_data (G_OBJECT (combo), "tag", (gpointer) text);
format (GTK_WIDGET (combo), textview);
gtk_combo_box_set_active (combo, -1);
}
/* Remove all of the tags from the selected text. */
static void
clear_clicked (GtkButton *button,
GtkTextView *textview)
{
GtkTextIter start, end;
GtkTextBuffer *buffer;
buffer = gtk_text_view_get_buffer (textview);
gtk_text_buffer_get_selection_bounds (buffer, &start, &end);
gtk_text_buffer_remove_all_tags (buffer, &start, &end);
}
텍스트 태그를 생성할 때는 보통 GtkTextBuffer의 태그 테이블로 추가해야 하는데, 이는 텍스트 버퍼에서 이용 가능한 모든 태그를 보유하는 객체에 해당한다. gtk_text_tag_new()를 이용하면 새로운 GtkTextTag 객체를 생성한 후 태그 테이블로 추가할 수 있다. 하지만 gtk_text_buffer_create_tag()를 이용하면 이 모든 작업을 한 번에 수행할 수 있다.
GtkTextTag* gtk_text_buffer_create_tag (GtkTextBuffer *buffer,
const gchar *tag_name,
const gchar *first_property_name,
...);
해당 함수의 첫 번째와 두 번째 매개변수는 GtkTextTag가 추가될 태그 테이블에 대한 텍스트 버퍼를 비롯해 텍스트 태그에 부여할 이름을 명시하도록 해준다. 이러한 이름은 더 이상 GtkTextTag 객체를 이용할 필요가 없을 때 태그를 참조하는 데 이용할 수 있다. 다음 매개변수 집합은 NULL로 끝나는 GtkTextTag 스타일 프로퍼티 리스트와 그 값이다.
예를 들어, 배경색과 전경색을 각각 검정색과 흰색으로 설정하는 텍스트 태그를 생성하고 싶다면 아래 함수를 이용할 수 있겠다. 이 함수는 생성된 텍스트 태그를 리턴하지만 해당 텍스트 태그는 이미 텍스트 버퍼의 태그 테이블로 추가되어 있을 것이다.
tag = gtk_text_buffer_create_tag (buffer, "colors", "background", "#000000",
"foreground", "#FFFFFF", NULL);
GTK+에서 이용 가능한 스타일 프로퍼티의 수는 엄청나다. GtkTextTag 스타일의 전체 리스트는 부록 C를 참조한다. 표는 각 프로퍼티명과 간략한 사용법, 프로퍼티가 수락하는 값의 타입을 알려줄 것이다.
텍스트 태그를 생성하여 GtkTextBuffer의 태그 테이블로 추가했다면 텍스트의 범위로 적용할 수 있겠다. 리스팅 7-7에서는 버튼이 클릭되면 선택된 텍스트로 태그가 적용된다. 선택된 텍스트가 없다면 커서 위치가 스타일로 설정될 것이다. 해당 위치에 입력된 모든 텍스트도 태그가 적용될 것이다.
태그는 주로 gtk_text_buffer_apply_tag_by_name()을 통해 텍스트로 적용된다. 태그는 start 와 end 반복자 사이의 텍스트로 적용된다. GtkTextTag 객체로 여전히 접근이 가능하다면 gtk_text_buffer_apply_tag()를 이용해 태그를 적용하는 수도 있다.
void gtk_text_buffer_apply_tag_by_name (GtkTextBuffer *buffer,
const gchar *tag_name,
const GtkTextIter *start,
const GtkTextIter *end);
리스팅 7-7에서 사용되진 않았지만 gtk_text_buffer_remove_tag_by_name()을 이용해 텍스트의 영역으로부터 태그를 제거하는 것이 가능하다. 이 함수는 두 개의 반복자가 존재할 경우 그 사이에 있는 모든 태그를 제거할 것이다.
void gtk_text_buffer_remove_tag_by_name (GtkTextBuffer *buffer,
const gchar *name,
const GtkTextIter *start,
const GtkTextIter *end);
이러한 함수들은 특정 텍스트 범위의 태그만 제거한다. 태그가 명시된 범위 이상의 텍스트로 추가되었다면 태그는 좀 더 작은 범위에서 제거될 것이며, 선택내용의 양쪽 중 한쪽에서 새로운 범위(bounds)가 생성될 것이다. 이는 리스팅 7-7의 애플리케이션으로 시험해볼 수 있겠다.
GtkTextTag 객체로 접근할 수 있다면 gtk_text_buffer_remove_tag()를 이용해 태그를 제거하는 것이 가능하다. 또 gtk_text_buffer_remove_all_tags()를 통해서는 범위 내 모든 태그를 삭제할 수 있다.
이미지 삽입하기
일부 애플리케이션에서는 이미지를 텍스트 버퍼로 삽입해야 하는 경우가 있다. 이는 GdkPixbuf 객체를 이용하면 쉽게 실행할 수 있다. 그림 7-8은 두 개의 이미지가 GdkPixbuf 객체로서 텍스트 버퍼에 삽입된 모습이다.
pixbuf는 세 단계를 거쳐 GtkTextBuffer로 추가된다. 먼저 pixbuf 객체를 생성하고 그것을 삽입할 GtkTextIter 를 검색해야 한다. 이후 gtk_text_buffer_insert_pixbuf()를 이용해 버퍼로 추가한다. 리스팅 7-8은 파일에서 GdkPixbuf 객체를 생성하여 텍스트 버퍼로 추가하는 과정을 표시한다.
리스팅 7-8. 이미지를 텍스트 버퍼로 삽입하기 (images.c)
#include <gtk/gtk.h>
#define IMAGE_UNDO "/path/to/undo.png"
#define IMAGE_REDO "/path/to/redo.png"
int main (int argc,
char *argv[])
{
GtkWidget *window, *scrolled_win, *textview;
GdkPixbuf *undo, *redo;
GtkTextIter line;
GtkTextBuffer *buffer;
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (window), "Pixbufs");
gtk_container_set_border_width (GTK_CONTAINER (window), 10);
gtk_widget_set_size_request (window, 200, 150);
textview = gtk_text_view_new ();
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (textview));
gtk_text_buffer_set_text (buffer, " Undo\n Redo", -1);
/* Create two images and insert them into the text buffer. */
undo = gdk_pixbuf_new_from_file (IMAGE_UNDO, NULL);
gtk_text_buffer_get_iter_at_line (buffer, &line, 0);
gtk_text_buffer_insert_pixbuf (buffer, &line, undo);
redo = gdk_pixbuf_new_from_file (IMAGE_REDO, NULL);
gtk_text_buffer_get_iter_at_line (buffer, &line, 1);
gtk_text_buffer_insert_pixbuf (buffer, &line, redo);
scrolled_win = gtk_scrolled_window_new (NULL, NULL);
gtk_container_add (GTK_CONTAINER (scrolled_win), textview);
gtk_container_add (GTK_CONTAINER (window), scrolled_win);
gtk_widget_show_all (window);
gtk_main();
return 0;
}
gtk_text_buffer_insert_pixbuf()를 이용하면 GdkPixbuf 객체를 텍스트 버퍼로 추가할 수 있다. GdkPixbuf 객체는 명시된 위치로 추가되는데, 그 위치는 버퍼 내 유효한 텍스트 반복자라면 어디든 가능하다.
void gtk_text_buffer_insert_pixbuf (GtkTextBuffer *buffer,
GtkTextIter *iter,
GdkPixbuf *pixbuf);
Pixbuf는 함수마다 다르게 처리된다. 가령 gtk_text_buffer_get_slice()는 pixbuf가 위치한 곳에 0xFFFC 문자를 놓을 것이다. 하지만 0xFFFC 문자는 버퍼에서 실제 문자로 발생할 수도 있기 때문에 pixbuf의 위치를 나타내기엔 신뢰도가 부족하다.
또 다른 예로 gtk_text_buffer_get_text()를 들 수 있는데, 이는 비텍스트 요소는 완전히 무시하므로 해당 함수를 이용해 텍스트에서 pixbuf를 확인할 수 있는 방법은 없다.
따라서 GtkTextBuffer에서 pixbuf를 이용하고 있을 경우 버퍼로부터 텍스트를 검색하는 최선의 방법은 gtk_text_buffer_get_slice()를 이용하는 것이다. 그 다음에 gtk_text_iter_get_pixbuf()를 이용하면 0xFFFC 문자가 GdkPixbuf 객체를 나타내는지 여부를 확인할 수 있는데, 해당 위치에서 pixbuf를 찾을 수 없을 경우 NULL을 리턴할 것이다.
GdkPixbuf* gtk_text_iter_get_pixbuf (const GtktTextIter *iter);
자식 위젯 삽입하기
위젯을 텍스트 버퍼로 삽입하는 작업은 pixbuf를 삽입하는 경우보다 약간 복잡한데, 그 이유는 개발자가 텍스트 버퍼와 텍스트 뷰에게 위젯을 포함시키도록 통지해야 하기 때문이다. 우선 GtkTextBuffer 내에서 위젯의 위치를 표시하는 데 사용할 GtkTextChildAnchor 객체를 생성한다. 이것이 완료되면 위젯을 GtkTextView 위젯으로 추가한다.
그림 7-9는 GtkButton 이라는 자식 위젯을 포함하는 GtkTextView 위젯의 모습을 보여준다. 리스팅 7-9를 이용하면 이 창을 생성할 수 있다. 버튼을 클릭하면 gtk_main_quit()이 호출되어 애플리케이션을 종료한다.
리스팅 7-9. 텍스트 버퍼에 자식 위젯 삽입하기 (childwidgets.c)
#include <gtk/gtk.h>
int main (int argc,
char *argv[])
{
GtkWidget *window, *scrolled_win, *textview, *button;
GtkTextChildAnchor *anchor;
GtkTextIter iter;
GtkTextBuffer *buffer;
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (window), "Child Widgets");
gtk_container_set_border_width (GTK_CONTAINER (window), 10);
gtk_widget_set_size_request (window, 250, 100);
textview = gtk_text_view_new ();
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (textview));
gtk_text_buffer_set_text (buffer, "\n Click to exit!", -1);
/* Create a new child widget anchor at the specified iterator. */
gtk_text_buffer_get_iter_at_offset (buffer, &iter, 8);
anchor = gtk_text_buffer_create_child_anchor (buffer, &iter);
/* Insert a GtkButton widget at the child anchor. */
button = gtk_button_new_with_label ("the button");
gtk_text_view_add_child_at_anchor (GTK_TEXT_VIEW (textview), button, anchor);
g_signal_connect_swapped (G_OBJECT (button), "clicked",
G_CALLBACK (gtk_widget_destroy),
(gpointer) window);
scrolled_win = gtk_scrolled_window_new (NULL, NULL);
gtk_container_add (GTK_CONTAINER (scrolled_win), textview);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_win),
GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
gtk_container_add (GTK_CONTAINER (window), scrolled_win);
gtk_widget_show_all (window);
gtk_main();
return 0;
}
GtkTextChildAnchor를 생성할 때는 이를 초기화한 후 GtkTextBuffer로 삽입할 필요가 있다. 이 작업은 gtk_text_buffer_create_child_anchor()를 호출하면 가능하다.
GtkTextChildAnchor* gtk_text_buffer_create_child_anchor (GtkTextBuffer *buffer,
GtkTextIter *iter);
자식 앵커(anchor)는 명시된 텍스트 반복자의 위치에서 생성된다. 이러한 자식 앵커는 자식 위젯을 텍스트 버퍼 내에 해당 위치로 추가할 수 있음을 GTK+에게 알려주는 마크(mark)에 불과하다.
다음으로 gtk_text_view_add_child_at_anchor()를 이용해 앵커 지점에 자식 위젯을 추가해야 한다. GdkPixbuf 객체와 마찬가지로 자식 위젯은 0xFFFC 문자로 표시된다. 즉, 해당 문자가 보이면 개발자는 그것이 자식 위젯인지 pixbuf인지 확인해야 한다는 뜻인데, 확인하지 않으면 둘의 구별이 불가하기 때문이다.
void gtk_text_view_add_child_at_anchor (GtkTextView *textview,
GtkWidget *child,
GtkTextChildAnchor *anchor);
자식 위젯이 0xFFFC 문자의 위치에 있는지 확인하려면 gtk_text_iter_get_child_anchor()를 호출해야 하는데, 자식 앵커가 그 위치에 없다면 NULL이 리턴될 것이다.
GtkTextChildAnchor* gtk_text_iter_get_child_anchor (const GtkTextIter *iter);
다음으로 gtk_text_child_anchor_get_widgets()를 이용해 앵커 지점에 추가된 위젯의 리스트를 검색할 수 있다. 하나의 앵커에는 하나의 자식 위젯만 추가할 수 있기 때문에 리턴된 리스트는 보통 하나의 요소만 포함할 것이란 사실을 주목해야 한다.
GList* gtk_text_child_anchor_get_widgets (GtkTextChildAnchor *anchor);
단, 여러 개의 텍스트 뷰에 동일한 버퍼를 이용하는 경우는 예외다. 이런 경우, 어떤 텍스트 뷰도 하나 이상의 위젯을 포함하지 않는다는 조건에 한해 다중 위젯을 텍스트 뷰 내에서 동일한 앵커로 추가할 수 있다. 그 이유는 자식 위젯이 텍스트 버퍼 대신 텍스트 뷰에 의해 처리되는 앵커로 추가되기 때문이다. 위젯 리스트의 사용이 끝나면 g_list_free()를 이용해 해제해야 한다.
GtkSourceView
GtkSourceView는 사실상 GTK+ 라이브러리에 속하지 않는 위젯이다. 이는 외부 라이브러리로서 GtkTextView 위젯을 확장 시 사용된다. GEdit를 사용한 경험이 있다면 GtkSourceView 위젯의 사용에 익숙할 것이다.
GtkSourceView 위젯이 텍스트 뷰로 추가하는 기능에는 여러 가지가 있다. 그 중 주목할 만한 기능은 다음과 같다.
- 행 번호붙이기(line numbering)
- 여러 프로그래밍 언어와 스크립팅 언어를 위한 구문 강조
- 구문 강조를 포함하는 문서의 인쇄 지원
- 자동 들여쓰기
- 괄호 일치시키기 (bracket matching)
- 실행취소(Undo)/재실행(Redo) 지원
- 소스 코드 내 위치 표시를 위한 소스 마커(source marker)
- 현재 행 강조하기
그림 7-10은 GtkSourceView 위젯을 이용한 GEdit의 스크린샷 모습이다. 행 번호붙이기, 구문 강조, 괄호 일치, 행 강조 기능이 켜진 상태다.
GtkSourceView 라이브러리는 구분된 API 문서를 갖고 있으며, http://gtksourceview.sourceforget.net에서 확인할 수 있다. 해당 라이브러리를 이용하는 애플리케이션을 컴파일해야 한다면 컴파일 명령에 'pkg-config --cflags --libs gtksourceview-1.0'을 추가하라.
GTK+ 애플리케이션에 구문 강조가 필요한 경우 처음부터 자신만의 위젯을 생성하는 대신 GtkSourceView 라이브러리를 이용하는 것도 하나의 방법이겠다.
자신의 이해도 시험하기
아래 연습문제는 기본 기능의 텍스트 편집 애플리케이션을 생성하도록 지시한다. GtkTextView 위젯과 상호작용할 수 있는 실습을 제공할 것이다.
연습문제 7-1. 텍스트 에디터
GtkTextView 위젯을 이용해 간단한 텍스트 에디터를 생성하라. 새 문서 생성하기, 파일 열기, 파일 저장하기, 문서 찾기, 텍스트 자르기, 텍스트 복사하기, 텍스트 붙여넣기를 포함해 여러 가지의 텍스트 편집 함수를 실행하는 기능을 제공해야 한다.
새 문서를 생성할 때는 모든 변경내용이 손실될 수도 있으므로 사용자가 실제로 계속하길 원하는지 확신할 수 있어야 한다. Save 버튼을 클릭하면 항상 파일을 어디에 저장할 것인지 물어야 한다. 연습문제를 완료했다면 부록 F의 해답을 참조한다.
힌트: 지금까지 소개한 여느 예제보다 훨씬 큰 GTK+ 애플리케이션에 해당하기 때문에 코드를 작성하기 전에 시간을 들여 연습장에 해답을 세부적으로 적어보길 바란다. 이것을 완료하면 한 번에 하나의 함수를 구현하고 완전히 작동하도록 확신한 후에 다음 기능으로 넘어가도록 한다. 해당 연습문제는 뒷부분에서 확장되기 때문에 가능한 한 편리한 해답을 도출하라!
이것은 이 책을 통틀어 처음으로 생성하는 텍스트 애플리케이션이다. 이제부터 여러 장에 걸쳐 완전한 기능을 가진 텍스트 에디터를 생성하는 데 도움이 되는 새로운 요소들을 학습할 것이다.
이 애플리케이션은 먼저 제 9장에서 확장되어 메뉴와 툴바를 추가할 것이다. 제 12장에서는 인쇄 지원, 그리고 최근에 열린 파일 및 검색내역을 기억하는 기능을 추가할 것이다.
연습문제 7-1에 가능한 한 가지 해답은 부록 F에 실려 있다. 텍스트 에디터 해답의 기능 중 대다수는 이번 장의 다른 예제를 통해 구현되었다. 따라서 해답의 상당 부분이 눈에 익을 것이다. 제시된 해답은 최소한의 해답이니 연습문제의 기본 요구조건을 확장시켜 더 연습해볼 것을 권한다.
요약
이번 장에서는 GtkTextView 위젯에 관한 모든 것을 학습하여 여러 행으로 된 텍스트를 표시할 수 있었다. 텍스트 뷰는 주로 GtkScrolledWindow에 의해 포함되는데, 이것은 스크롤링 기능을 구현하도록 자식 위젯에게 스크롤바를 제공하는 특수 타입의 GtkBin 컨테이너다.
GtkTextBuffer는 뷰 내에서 텍스트를 처리한다. 텍스트 버퍼는 개발자가 텍스트 태그를 이용해 텍스트 전체 또는 일부에 대한 여러 가지 프로퍼티를 변경할 수 있도록 해준다. 이는 자르기, 복사하기, 붙여넣기 함수를 제공하기도 한다.
GtkTextIter 객체를 이용하여 텍스트 버퍼를 이동할 수도 있지만, 텍스트 버퍼가 변경되면 텍스트 반복자는 무효해진다. 텍스트 반복자를 이용해 문서의 전방향 및 후방향 검색이 가능하다. 버퍼가 변경되어도 위치를 그대로 유지하려면 텍스트 마크를 이용해야 한다. 텍스트 뷰에는 텍스트 뿐만 아니라 이미지와 자식 위젯을 표시하는 기능도 겸비되어 있다. 자식 위젯은 텍스트 버퍼에서 앵커 지점에 추가된다.
본 장의 마지막 절을 통해서는 GtkTextView 위젯의 기능을 확장하는 GtkSourceView 위젯을 간략하게 소개하였다. 이는 구문 강조와 행 번호붙이기와 같은 기능이 필요할 때 이용할 수 있겠다.
제 8장에서는 새로운 위젯 두 가지, 콤보 박스와 트리 뷰를 소개할 것이다. 콤보 박스는 드롭다운 리스트에서 하나의 옵션을 선택하도록 해준다. 트리 뷰는 주로 스크롤이 있는 창에 포함된 리스트에서 하나 또는 그 이상의 옵션 선택을 허용한다. GtkTreeView는 본문에서 다루는 내용 중 가장 까다로운 위젯에 해당하므로 제 8장은 충분한 시간을 들여 학습하길 바란다.