Taryn Fox
jewelfox@fursona.net 2012
단추와 단추를 누르면 동작하는 다른 위젯을 만듭니다.
조성호
shcho@gnome.org 2017-2019.

# 3. 시그널 취하기

        이전 따라하기 과정에서 레이블, 그림, 단추 위젯을 만드는 방법을 알아보았습니다. 여기서는 단추를 누르거나 어떤 반응을 받았을 때 보내는 시그널을 처리할 함수를 작성하여 단추와 기타 입력 위젯을 만드는 방법을 알아보겠습니다.

# 기본 프로그램

그놈에서 단추, 스위치 같이 다룰 수 있는 위젯은 누르거나 마우스 커서를 올려두는 식으로 활성화하면 시그널을 내보냅니다. 예를 들어, 단추라면 누군가가 단추를 누르면 "clicked" 시그널을 내보냅니다. 시그널이 나오면, 그놈에서는 여러분이 작성한 코드에서 시그널에 반응하는 부분을 찾습니다.

코드를 어떻게 작성할까요? 단추의 "clicked" 시그널을 콜백 함수에 연결하면, 해당 함수에서 시그널을 처리합니다. 따라서 시그널을 줄 때마다 시그널에 연결한 함수가 동작하죠.

매우 기본적인 예제입니다:

ApplicationWindow에는 단추와 레이블이 있는데 이걸 그리드로 정렬했습니다. 단추를 클릭할 때마다 과자 갯수 값을 가진 변수 값이 하나씩 늘어나고, 과자 갯수를 보여주는 레이블이 바뀝니다.

이 예제의 과자는 웹사이트에서 로그인 정보를 저장하고 방문 사이트 상태를 보관하는 그런 과자가 아닙니다. 그냥 상상속의 무언가일 뿐이죠. 원하면 실제로 구워드세요.

하여간, 윈도우와 위젯 만들기를 시작하기 전 프로그램을 시작하는 기본 기반 코드를 작성하겠습니다. 프로그램에 고유 이름을 붙이고, 기반 코드에서 확 바뀌는 부분은 과자 갯수 값을 보관하는 코드 시작 부분의 전역 변수 추가입니다.

#!/usr/bin/gjs

imports.gi.versions.Gtk = '3.0'; const Gtk = imports.gi.Gtk;

// We start out with 0 cookies var cookies = 0;

class GettingTheSignal { // Create the application itself constructor() { this.application = new Gtk.Application();

// Connect 'activate' and 'startup' signals to the callback functions this.application.connect('activate', this._onActivate.bind(this)); this.application.connect('startup', this._onStartup.bind(this)); }

// Callback function for 'activate' signal presents window when active _onActivate() { this._window.present(); }

// Callback function for 'startup' signal builds the UI _onStartup() { this._buildUI (); }

프로그램의 connect 메서드와 bind 부분을 보시죠. 창을 나타내고 사용자 인터페이스를 구성하는 함수에 activate 시그널과 startup 시그널을 연결하죠. 단추에도 "clicked" 시그널에 연결한다는 점만 빼고는 같은 방식을 취합니다.

# 단추 누르기

보통 때와 같이, 프로그램을 시작할 때 호출하는 _buildUI 함수에, 단추와 다른 위젯을 만드는 모든 코드를 넣겠습니다.

// Build the application's UI _buildUI() {

우선, 창을 만들고요:

// Create the application window this._window = new Gtk.ApplicationWindow({ application: this.application, window_position: Gtk.WindowPosition.CENTER, default_height: 200, default_width: 400, title: "Click the button to get a cookie!"});

여기서 default_height, default_width 속성을 설정한 걸 보시죠. ApplicationWindow의 너비 높이를 픽셀 단위로 설정합니다.

다음, 과자 갯수를 보여줄 레이블을 만들겠습니다. 레이블의 label 속성 일부로 cookies 변수를 사용할 수 있습니다.

// Create the label this._cookieLabel = new Gtk.Label ({ label: "Number of cookies: " + cookies });

다음, 단추를 만들겠습니다. 단추에 표시할 텍스트를 보여줄 때 label 속성 값을 설정할 수 있고, 프로그램 사용자 인터페이스를 구성하고 나서 작성할 _getACookie 함수에 "clicked" 시그널을 연결하겠습니다.

// Create the cookie button this._cookieButton = new Gtk.Button ({ label: "Get a cookie" });

// Connect the cookie button to the function that handles clicking it this._cookieButton.connect ('clicked', this._getACookie.bind(this));

마지막으로, 그리드를 만들어 레이블과 단추를 붙여넣은 후, 그리드에 붙여넣은 위젯을 창 위에 표시하도록 창에 추가하겠습니다. 이게 _buildUI 함수에서 작성할 모든 내용이니, 중괄호로 닫으세요. 쉼표를 입력해서 다음 함수로 넘어간다는걸 그놈에 알려주시고요. 레이블 코드를 먼저 작성하겠지만, 해당 코드 아래에서 그리드에 붙여넣을 수 있습니다.

// Create a grid to arrange everything inside this._grid = new Gtk.Grid ({ halign: Gtk.Align.CENTER, valign: Gtk.Align.CENTER, row_spacing: 20 });

// Put everything inside the grid this._grid.attach (this._cookieButton, 0, 0, 1, 1); this._grid.attach (this._cookieLabel, 0, 1, 1, 1);

// Add the grid to the window this._window.add (this._grid);

// Show the window and all child widgets this._window.show_all();

}

이제 _getACookie 함수를 작성하겠습니다. 단추에서 "clicked" 시그널을 보낼 따마다 이 함수의 코드를 실행합니다. 이 때 과자 갯수가 하나씩 늘어나며, 레이블에 새 과자 갯수 값이 들어가서 바뀝니다. 이 동작은 레이블의 set_label 메서드로 처리하겠습니다.

여러 위젯에 동일한 속성과 메서드가 있습니다. 레이블 및 단추에도 어떤 텍스트를 넣는지 나타내는 lable 속성이 있고, 어떤 텍스트가 있는지 확인하는 get_label과 텍스트를 바꾸는 set_label 메서드가 있습니다. 따라서 위젯 동작 방식을 알면 다른 위젯 동작 방식도 알게 되는거죠??.

_getACookie: function() {

// Increase the number of cookies by 1 and update the label cookies++; this._cookieLabel.set_label ("Number of cookies: " + cookies);

}

};

마지막으로, 이전 따라하기 예제에서 코드를 실행했던 방식으로 프로그램을 실행하겠습니다.

// Run the application let app = new GettingTheSignal (); app.application.run (ARGV);

# 스위치 젖히기

GTK+ 도구함에는 단추만 있는게 아닙니다. 이 예제에서와 같이 스위치도 있구요~. 스위치는 레이블 속성이 없어서 스위치가 어떤 역할을 하는지 레이블을 옆에 따로 만들어야합니다.

스위치 단추의 위치는, 끔 아니면 켬 두가지가 있죠. 스위치를 켠 상태로 두면 텍스트와 배경 색이 바뀌어서 어떤 상태인지 확인할 수 있습니다.

큰 글씨를 표시하고 화면 키보드를 켜고 끄는 기능을 갖춘 그놈 접근성 메뉴 같은데서 이런 스위치를 볼 수 있죠. 여기서 스위치로 가상의 과자 뽑기 기계 동작 여부를 다루겠습니다. 스위치를 켜두면 "Get a cookie" 단추를 눌렀을 때 과자를 받을 수 있습니다. 스위치를 끄면 단추를 눌렀을 때 반응하지 않고요.

화면의 우측 상단에 사용자 이름이 있는 사람 모양 아이콘을 누르면 접근성 메뉴로 들어갈 수 있습니다.

이제 스위치를 어떻게 만드는지 알아보죠:

// Create the switch that controls whether or not you can win this._cookieSwitch = new Gtk.Switch ();

스위치에서 뭘 할지는 실제로 연결할 필요가 없습니다. 우리가 할 일은 _getACookie 함수에서 if 구문을 작성해서 스위치가 켜졌는지 여부만 확인하면 됩니다. 스위치를 젖혔을 때 바로 무언가를 하려면 notify::active 시그널을 다음처럼 연결하면 됩니다:

// Connect the switch to the function that handles it this._cookieSwitch.connect ('notify::active', this._cookieDispenser.bind(this));

스위치는 기본적으로 끔 위치로 설정해둡니다. 켠 상태로 시작하게 하려면, 스위치를 만들 때 active 속성을 true로 설정하면 됩니다.

this._cookieSwitch = new Gtk.Switch ({ active: true });

그럼 평범하게 만들어보고 옆에 레이블을 붙여두죠. 스위치와 레이블은 오른편에 두는걸로 하겠습니다. 그렇게 해서, 이 위젯에 맞는 그리드를 만들고 이 그리드를 큰 그리드에 넣겠습니다. 다 만들고 나면 코드가 다음과 같이 되겠죠:

// Create the switch that controls whether or not you can win this._cookieSwitch = new Gtk.Switch ();

// Create the label to go with the switch this._switchLabel = new Gtk.Label ({ label: "Cookie dispenser" });

// Create a grid for the switch and its label this._switchGrid = new Gtk.Grid ({ halign: Gtk.Align.CENTER, valign: Gtk.Align.CENTER });

// Put the switch and its label inside that grid this._switchGrid.attach (this._switchLabel, 0, 0, 1, 1); this._switchGrid.attach (this._cookieSwitch, 1, 0, 1, 1);

마찬가지로 이제 더 큰 그리드에 몽땅 정리하겠습니다.

// Put everything inside the grid this._grid.attach (this._cookieButton, 0, 0, 1, 1); this._grid.attach (this._switchGrid, 0, 1, 1, 1); this._grid.attach (this._cookieLabel, 0, 2, 1, 1);

이제 _getACookie 함수 코드를 바꿔서 과자 뽑기가 켜졌는지 확인하겠습니다. 스위치의 get_active 메서드를 사용하겠습니다. 스위치가 켜져있으면 true, 꺼져있으면 false를 반환합니다.

if 구문에서 다음과 같이 메서드를 사용하면, 메서드에서 true를 반환했을 때 if 구문 안에 있는 코드를 실행합니다.

_getACookie() {

// Is the cookie dispenser turned on? if (this._cookieSwitch.get_active()) {

// Increase the number of cookies by 1 and update the label cookies++; this._cookieLabel.set_label ("Number of cookies: " + cookies);

}

}

# 라디오 튜닝

우리가 쓸 수 있는 다른 입력 위젯으로 RadioButton이 있습니다. 한 모임 단위로 만들 수 있고, 모임 안에 있는 RadioButton은 한번에 하나만 선택할 수 있습니다. 구식 카 라디오의 프리셋 단추 처럼 동작하기 때문에 RadioButton으로 부릅니다. 라디오는 한번에 방송국 하나만 선택할 수 있기에, 어떤 단추를 누르면 다른 단추는 도로 튀어나옵니다.

우선 ApplicationWindow의 이름을 바꾸고 border_width 속성 값을 늘려서, 위젯이 답답하게 붙어있지 않게 하겠습니다. border_width는 위젯과 창 가장자리 사이의 픽셀 단위 간격입니다.

// Create the application window this._window = new Gtk.ApplicationWindow({ application: this.application, window_position: Gtk.WindowPosition.CENTER, default_height: 200, default_width: 400, border_width: 20, title: "Choose the one that says 'cookie'!"});

그 다음 RadioButton을 만들겠습니다. 근데 그룹안에 어떻게 만들었죠? 우리가 할 일은 각 RadioButton의 그룹 속성에 다른 RadioButton 이름을 넣는 일입니다.

// Create the radio buttons this._cookieRadio = new Gtk.RadioButton ({ label: "Cookie" }); this._notCookieOne = new Gtk.RadioButton ({ label: "Not cookie", group: this._cookieRadio }); this._notCookieTwo = new Gtk.RadioButton ({ label: "Not cookie", group: this._cookieRadio });

다음 RadioButton의 그리드를 만들겠습니다. 만든 순서대로 그리드에 똑같이 정렬할 필요는 없습니다.

// Arrange the radio buttons in their own grid this._radioGrid = new Gtk.Grid (); this._radioGrid.attach (this._notCookieOne, 0, 0, 1, 1); this._radioGrid.attach (this._cookieRadio, 0, 1, 1, 1); this._radioGrid.attach (this._notCookieTwo, 0, 2, 1, 1);

보통 기본으로 선택한 RadioButton은 그룹 이름으로 설정한 RadioButton입니다. 우선은 기본적으로 "Not cookie" 단추를 선택한 상태로 두려고 합니다. 그러니까 set_active 메서드를 쓰겠습니다.

RadioButton을 만들 때 active 속성을 설정할 수도 있습니다.

// Set the button that will be at the top to be active by default this._notCookieOne.set_active (true);

이제 흔히 하던대로 메인 그리드에 모든 위젯을 정리하겠습니다 ...

// Put everything inside the grid this._grid.attach (this._radioGrid, 0, 0, 1, 1); this._grid.attach (this._cookieButton, 0, 1, 1, 1); this._grid.attach (this._cookieLabel, 0, 2, 1, 1);

그리고 과자 단추를 선택했는지 확인하도록 _getACookie 함수 코드를 바꾸겠습니다.

_getACookie() {

// Did you select "cookie" instead of "not cookie"? if (this._cookieRadio.get_active()) {

// Increase the number of cookies by 1 and update the label cookies++; this._cookieLabel.set_label ("Number of cookies: " + cookies);

}

}

# "cookie" 쓸 수 있죠?

우리가 다룰 마지막 위젯은 단일 행 텍스트 항목을 다루는 Entry 위젯입니다.

텍스트 편집기를 만들 때 처럼 한 문당 이상을 입력할 수 있는게 필요하다면, 개별적으로 이리저리 자유롭게 바꿀 수 있는 TextView 위젯을 찾으시려는 지도 모르겠습니다.

창 이름을 바꾸고 나서 Entry 위젯을 만들겠습니다.

// Create the text entry field this._spellCookie = new Gtk.Entry ();

그 다음, 그리드에 모두 정리하고요 ...

// Put everything inside the grid this._grid.attach (this._spellCookie, 0, 0, 1, 1); this._grid.attach (this._cookieButton, 0, 1, 1, 1); this._grid.attach (this._cookieLabel, 0, 2, 1, 1);

Entry의 get_text 메서드로 텍스트를 가져와서 "cookie" 문자열이 맞는지 확인하도록 _getACookie의 if 구문을 수정하겠습니다. "cookie"의 어디가 대문자인지는 상관하지 않을거기에 if 구문 안에서 Entry의 모든 텍스트 문자를 소문자로 바꾸는 JavaScript 내장 함수 toLowerCase를 사용하겠습니다.

Entry 위젯은 사용자가 못바꾸는 텍스트 문자열을 설정하는 label 속성이 없습니다(심지어 단추에도 보통은 레이블의 문자열을 바꿀 수 없습니다). 대신 사용자가 입력한 내용이 일치하는지 여부를 바꾸는 text 속성이 있습니다.

_getACookie() {

// Did you spell "cookie" correctly? if ((this._spellCookie.get_text()).toLowerCase() == "cookie") {

// Increase the number of cookies by 1 and update the label cookies++; this._cookieLabel.set_label ("Number of cookies: " + cookies);

}

}

# 다음은요?

과자 만들기 프로그램의 여러가지 각 버전에 해당하는 완전한 코드를 보려면 계속 읽어나가십시오.

메인 JavaScript 지침서 페이지에는 여기서 다루지 못한 여러가지 입력 위젯을 다룬 상세 예제 코드가 있습니다.

# 완전한 예제 코드

## 단추 코드 예제

#!/usr/bin/gjs

imports.gi.versions.Gtk = '3.0'; const Gtk = imports.gi.Gtk;

// We start out with 0 cookies var cookies = 0;

class GettingTheSignal {

// Create the application itself constructor() { this.application = new Gtk.Application();

// Connect 'activate' and 'startup' signals to the callback functions this.application.connect('activate', this._onActivate.bind(this)); this.application.connect('startup', this._onStartup.bind(this)); }

// Callback function for 'activate' signal presents window when active _onActivate() { this._window.present(); }

// Callback function for 'startup' signal builds the UI _onStartup() { this._buildUI(); }

// Build the application's UI _buildUI() {

// Create the application window this._window = new Gtk.ApplicationWindow({ application: this.application, window_position: Gtk.WindowPosition.CENTER, default_height: 200, default_width: 400, title: "Click the button to get a cookie!"});

// Create the label this._cookieLabel = new Gtk.Label ({ label: "Number of cookies: " + cookies });

// Create the cookie button this._cookieButton = new Gtk.Button ({ label: "Get a cookie" });

// Connect the cookie button to the function that handles clicking it this._cookieButton.connect ('clicked', this._getACookie.bind(this));

// Create a grid to arrange everything inside this._grid = new Gtk.Grid ({ halign: Gtk.Align.CENTER, valign: Gtk.Align.CENTER, row_spacing: 20 });

// Put everything inside the grid this._grid.attach (this._cookieButton, 0, 0, 1, 1); this._grid.attach (this._cookieLabel, 0, 1, 1, 1);

// Add the grid to the window this._window.add (this._grid);

// Show the window and all child widgets this._window.show_all();

}

_getACookie() {

// Increase the number of cookies by 1 and update the label cookies++; this._cookieLabel.set_label ("Number of cookies: " + cookies);

}

};

// Run the application let app = new GettingTheSignal (); app.application.run (ARGV);

## 스위치 코드 예제

#!/usr/bin/gjs

imports.gi.versions.Gtk = '3.0'; const Gtk = imports.gi.Gtk;

// We start out with 0 cookies var cookies = 0;

class GettingTheSignal {

// Create the application itself constructor() { this.application = new Gtk.Application();

// Connect 'activate' and 'startup' signals to the callback functions this.application.connect('activate', this._onActivate.bind(this)); this.application.connect('startup', this._onStartup.bind(this)); }

// Callback function for 'activate' signal presents window when active _onActivate() { this._window.present(); }

// Callback function for 'startup' signal builds the UI _onStartup() { this._buildUI(); }

// Build the application's UI _buildUI() {

// Create the application window this._window = new Gtk.ApplicationWindow({ application: this.application, window_position: Gtk.WindowPosition.CENTER, default_height: 200, default_width: 400, title: "Click the button to get a cookie!"});

// Create the label this._cookieLabel = new Gtk.Label ({ label: "Number of cookies: " + cookies });

// Create the cookie button this._cookieButton = new Gtk.Button ({ label: "Get a cookie" });

// Connect the cookie button to the function that handles clicking it this._cookieButton.connect ('clicked', this._getACookie.bind(this));

// Create the switch that controls whether or not you can win this._cookieSwitch = new Gtk.Switch ();

// Create the label to go with the switch this._switchLabel = new Gtk.Label ({ label: "Cookie dispenser" });

// Create a grid for the switch and its label this._switchGrid = new Gtk.Grid ({ halign: Gtk.Align.CENTER, valign: Gtk.Align.CENTER });

// Put the switch and its label inside that grid this._switchGrid.attach (this._switchLabel, 0, 0, 1, 1); this._switchGrid.attach (this._cookieSwitch, 1, 0, 1, 1);

// Create a grid to arrange everything else inside this._grid = new Gtk.Grid ({ halign: Gtk.Align.CENTER, valign: Gtk.Align.CENTER, row_spacing: 20 });

// Put everything inside the grid this._grid.attach (this._cookieButton, 0, 0, 1, 1); this._grid.attach (this._switchGrid, 0, 1, 1, 1); this._grid.attach (this._cookieLabel, 0, 2, 1, 1);

// Add the grid to the window this._window.add (this._grid);

// Show the window and all child widgets this._window.show_all();

}

_getACookie() {

// Is the cookie dispenser turned on? if (this._cookieSwitch.get_active()) {

// Increase the number of cookies by 1 and update the label cookies++; this._cookieLabel.set_label ("Number of cookies: " + cookies);

}

}

};

// Run the application let app = new GettingTheSignal (); app.application.run (ARGV);

## RadioButton 코드 예제

#!/usr/bin/gjs

imports.gi.versions.Gtk = '3.0'; const Gtk = imports.gi.Gtk;

// We start out with 0 cookies var cookies = 0;

class GettingTheSignal {

// Create the application itself constructor() { this.application = new Gtk.Application();

// Connect 'activate' and 'startup' signals to the callback functions this.application.connect('activate', this._onActivate.bind(this)); this.application.connect('startup', this._onStartup.bind(this)); }

// Callback function for 'activate' signal presents window when active _onActivate() { this._window.present(); }

// Callback function for 'startup' signal builds the UI _onStartup() { this._buildUI(); }

// Build the application's UI _buildUI() {

// Create the application window this._window = new Gtk.ApplicationWindow({ application: this.application, window_position: Gtk.WindowPosition.CENTER, default_height: 200, default_width: 400, border_width: 20, title: "Choose the one that says 'cookie'!"});

// Create the radio buttons this._cookieRadio = new Gtk.RadioButton ({ label: "Cookie" }); this._notCookieOne = new Gtk.RadioButton ({ label: "Not cookie", group: this._cookieRadio }); this._notCookieTwo = new Gtk.RadioButton ({ label: "Not cookie", group: this._cookieRadio });

// Arrange the radio buttons in their own grid this._radioGrid = new Gtk.Grid (); this._radioGrid.attach (this._notCookieOne, 0, 0, 1, 1); this._radioGrid.attach (this._cookieRadio, 0, 1, 1, 1); this._radioGrid.attach (this._notCookieTwo, 0, 2, 1, 1);

// Set the button that will be at the top to be active by default this._notCookieOne.set_active (true);

// Create the cookie button this._cookieButton = new Gtk.Button ({ label: "Get a cookie" });

// Connect the cookie button to the function that handles clicking it this._cookieButton.connect ('clicked', this._getACookie.bind(this));

// Create the label this._cookieLabel = new Gtk.Label ({ label: "Number of cookies: " + cookies });

// Create a grid to arrange everything inside this._grid = new Gtk.Grid ({ halign: Gtk.Align.CENTER, valign: Gtk.Align.CENTER, row_spacing: 20 });

// Put everything inside the grid this._grid.attach (this._radioGrid, 0, 0, 1, 1); this._grid.attach (this._cookieButton, 0, 1, 1, 1); this._grid.attach (this._cookieLabel, 0, 2, 1, 1);

// Add the grid to the window this._window.add (this._grid);

// Show the window and all child widgets this._window.show_all();

}

_getACookie() {

// Did you select "cookie" instead of "not cookie"? if (this._cookieRadio.get_active()) {

// Increase the number of cookies by 1 and update the label cookies++; this._cookieLabel.set_label ("Number of cookies: " + cookies);

}

}

};

// Run the application let app = new GettingTheSignal (); app.application.run (ARGV);

## Entry 코드 예제

#!/usr/bin/gjs

imports.gi.versions.Gtk = '3.0'; const Gtk = imports.gi.Gtk;

// We start out with 0 cookies var cookies = 0;

class GettingTheSignal {

// Create the application itself constructor() { this.application = new Gtk.Application();

// Connect 'activate' and 'startup' signals to the callback functions this.application.connect('activate', this._onActivate.bind(this)); this.application.connect('startup', this._onStartup.bind(this)); }

// Callback function for 'activate' signal presents window when active _onActivate() { this._window.present(); }

// Callback function for 'startup' signal builds the UI _onStartup() { this._buildUI(); }

// Build the application's UI _buildUI() {

// Create the application window this._window = new Gtk.ApplicationWindow({ application: this.application, window_position: Gtk.WindowPosition.CENTER, default_height: 200, default_width: 400, border_width: 20, title: "Spell 'cookie' to get a cookie!"});

// Create the text entry field this._spellCookie = new Gtk.Entry ();

// Create the cookie button this._cookieButton = new Gtk.Button ({ label: "Get a cookie" });

// Connect the cookie button to the function that handles clicking it this._cookieButton.connect ('clicked', this._getACookie.bind(this));

// Create the label this._cookieLabel = new Gtk.Label ({ label: "Number of cookies: " + cookies });

// Create a grid to arrange everything inside this._grid = new Gtk.Grid ({ halign: Gtk.Align.CENTER, valign: Gtk.Align.CENTER, row_spacing: 20 });

// Put everything inside the grid this._grid.attach (this._spellCookie, 0, 0, 1, 1); this._grid.attach (this._cookieButton, 0, 1, 1, 1); this._grid.attach (this._cookieLabel, 0, 2, 1, 1);

// Add the grid to the window this._window.add (this._grid);

// Show the window and all child widgets this._window.show_all();

}

_getACookie() {

// Did you spell "cookie" correctly? if ((this._spellCookie.get_text()).toLowerCase() == "cookie") {

// Increase the number of cookies by 1 and update the label cookies++; this._cookieLabel.set_label ("Number of cookies: " + cookies);

}

}

};

// Run the application let app = new GettingTheSignal (); app.application.run (ARGV);