16장. GUI 애플리케이션

C로 구현한 파이썬에서는 그래픽 사용자 인터페이스(GUI)를 위해 Tkinter를 사용한다. 자바 플랫폼에는 스윙(Swing)이라는 GUI 도구가 기본으로 포함되어 있으므로, 자이썬에서도 자동적으로 얻을 수 있다. 그 외의 툴킷들을 사용하여 GUI를 작성할 수도 있는데, 그 점에 있어서는 CPython과 마찬가지이다. 최근에 쓰이는 자바 구현에서는 모두 스윙을 사용할 수 있도록 되어있기 때문에, 이 장에서는 스윙 GUI를 사용하는데에 초점을 맞출 것이다.

스윙은 큰 주제라서, 한 장에서 완벽하게 다룰 수는 없다. 사실, 스윙만을 위한 책도 있을 정도이다. 여기서는 자이썬에서 스윙의 사용을 설명하기 위해 충분할 만큼만 다루어보고자 한다. 스윙에 대한 깊이 있는 내용은 선 마이크로 시스템즈에서 제공하는 스윙 학습서 java.sun.com/docs/books/tutorial/uiswing 같은 책이나 웹 학습서를 참고하라.

자이썬에서 스윙을 사용하는 것은 자바에서 스윙을 사용하는 것보다 더 많은 장점이 있다. 예를 들어, 자이썬에서는 빈(bean) 속성이나 바인딩 액션이 그다지 까다롭지 않다(자바에서는 익명 클래스를 사용해야하는데 반해, 자이썬에서는 함수를 전달할 수 있다).

자바로 간단한 스윙 애플리케이션부터 만들어보고 나서, 자이썬으로 만든 동일한 애플리케이션을 살펴보도록 하겠다. 예제 16-1을 보자.

예제 16-1.

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;

public class HelloWorld {

    public static void main(String[] args) {
        JFrame frame = new JFrame("Hello Java!");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(300, 300);
        JButton button = new JButton("Click Me!");
        button.addActionListener(
            new ActionListener() {
                public void actionPerformed(ActionEvent event) {
                    System.out.println("Clicked!");
                }
            }
        );
        frame.add(button);
        frame.setVisible(true);
    }
}

이 간단한 애플리케이션은 JButton으로 완전히 채워진 JFrame을 그린다. 버튼을 누르면, “Click Me!”가 명령줄에 찍힌다. 그림 16-1을 보라.

picture_3

그림 16-1. 명령줄에 “Click Me” 출력

이제 이 프로그램이 자이썬으로는 어떻게 보이는지 보자(예제 16-2).

예제 16-2.

from javax.swing import JButton, JFrame

frame = JFrame('Hello, Jython!',
            defaultCloseOperation = JFrame.EXIT_ON_CLOSE,
            size = (300, 300)
        )

def change_text(event):
    print 'Clicked!'

button = JButton('Click Me!', actionPerformed=change_text)
frame.add(button)
frame.visible = True

제목을 제외하고, 애플리케이션은 동일한 JFrame과 JButton을 생성하고, 버튼을 클릭했을 때 “Click Me!”를 화면에 출력한다. 그림 16-2를 보라.

picture_2

**그림 16-2. ** 예제 16-2에서 생성된 애플리케이션의 결과

이제 자바와 자이썬 예제를 한줄 한줄 비교해보면서, 스윙 앱을 작성하는데 있어서 서로 어떤 차이가 있는지 느껴보도록 하자. 먼저 import 구문부터 살펴보자.

예제 16-3. 자바

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;

예제 16-4. 자이썬

from javax.swing import JButton, JFrame

자이썬에서는 from을 쓰는 것보다는, 이름을 써서 명시적으로 import 하는 것이 언제나 최선의 방법이다.

자이썬의 동적 타이핑 기능은, 굳이 필요하지 않은 ActionEvent나 ActionListener 클래스를 우리 코드로 들여오지 않도록 해준다.

다음은, JFrame을 생성하고 bean 속성 두개를 설정하는 코드이다.

예제 16-5. 자바

JFrame frame = new JFrame("Hello Java!");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(300, 300);

예제 16-6. 자이썬

frame = JFrame('Hello, Jython!',
            defaultCloseOperation = JFrame.EXIT_ON_CLOSE,
            size = (300, 300)
        )

자바에서는 새로운 JFrame이 만들어지고 나서 bean 속성인 defaultCloseOperation과 size가 설정된다. 자이썬에서는 bean 속성 설정자를 생성자 안에다가 바로 추가할 수 있다. 이러한 지름길에 대해서 6장에서 자세히 다루고 있다.여전히 어느 정도의 반복이 있는데, 그것은 스윙 라이브러리에 있어서 bean 속성이 너무나 중요하기 때문이다. 한 마디로, getFoo/setFoo 형식의 bean 반환자와 설정자를 사용한다면, 그것들을 개체의 속성인 것처럼 “foo.”이라는 이름을 가지고 다룰 수 있다.그래서 x.getFoo() 대신 x.foo를 사용할 수 있는 것이다. x.setFoo(bar) 대신에는 x.foo = bar를 사용할 수 있다. 여러분이 어떠한 스윙 앱을 살펴보더라도 다음과 같은 설정자 블록이 많은 부분을 차지하고 있음을 보게 될 것이다.

예제 16-7.

JTextArea t = JTextArea();
t.setText(message)
t.setEditable(false)
t.setWrapStyleWord(true)
t.setLineWrap(true)
t.setAlignmentX(Component.LEFT_ALIGNMENT)
t.setSize(300, 1)

우리 의견으로는, 자이썬에서 속성을 지정하는 관용적인 방법이 훨씬 읽기 편하고 보기도 좋다.

예제 16-8.

t = JTextArea()
t.text = message
t.editable = False
t.wrapStyleWord = True
t.lineWrap = True
t.alignmentX = Component.LEFT_ALIGNMENT
t.size = (300, 1)

설정자들을 생성자 내로 몰아넣을 수도 있다.

t = JTextArea(text = message,
              editable = False,
              wrapStyleWord = True,
              lineWrap = True,
              alignmentX = Component.LEFT_ALIGNMENT,
              size = (300, 1)
             )

속성들을 생성자 안에 말아넣을 때에는, 설정자가 호출되는 순서에 주의하여야 한다. bean 속성은 대개 순서에 종속적이지 않으므로 일반적으로는 문제가 되지 않는다. 주요한 예외는 SetVisible()이다. 속성을 설정하는 동안 이상한 일을 겪지 않으려면 가시적인 속성을 생성자 외부에서 설정해야 할 것이다. 본론으로 돌아가서, 그 다음 코드 블록은 JButton을 생성하고 그 버튼에 “Click Me!”를 출력하는 액션을 바인딩한다.

예제 16-9. 자바

JButton button = new JButton("Click Me!");
button.addActionListener(
    new ActionListener() {
        public void actionPerformed(ActionEvent event) {
            System.out.println("Clicked!");
        }
    }
);
frame.add(button);

예제 16-10. 자이썬

def change_text(event):
    print 'Clicked!'

button = JButton('Click Me!', actionPerformed=change_text)
frame.add(button)

우리는 이 부분에서 자이썬의 메소드가 자바에 비해 특히 낫다고 생각한다. 첫 번째 클래스 함수인 “change_text”를 생성자에서 직접 JButton에 전달할 수 있다. 이쪽이 익명의 ActionListener 클래스를 만들고 정적 유형 선언에 필요한 actionPerformed 메소드를 일일이 정의하는 자바의 성가신 “addActionListener”보다 낫다. 자이썬의 가독성이 정말로 돋보이는 경우라 하겠다. 이것은 addEvent()removeEvent() 메소드가 있는 자바코드의 이벤트를 자이썬이 자동적으로 감지하기 때문에 가능한 일이다. 이벤트 메소드가 공용(public)일 경우에, 자이썬은 이벤트의 이름을 가지고 훌륭한 파이썬 구문을 통해 그것을 접근 가능하게 만든다. 끝으로, 두 예제에서 가시성(visible)을 참으로 설정하도록 하겠다. 프레임의 생성자에서 visible 속성을 설정할 수도 있지만, visible 속성은 예외적으로 정확한 시점(이 예제에서는 마지막)에 설정할 필요가 있다.

예제 16-11. 자바

frame.setVisible(true);

예제 16-12. 자이썬

frame.visible = True

이제껏 간단한 예제를 살펴보았는데, 중간 크기의 앱은 자이썬에서 어떻게 보일지 살펴보는 것이 좋겠다. 요즘은 트위터 앱이 GUI 애플리케이션의 “Hello World”가 되었으니, 대세를 따르도록 하겠다. 다음 애플리케이션은 사용자의 로그인 입력을 받는다. 사용자가 성공적으로 로그인하면, 가장 최근의 이야기(tweet)를 표시한다. 예제 16-13을 보라.

예제 16-13. 자이썬 트위터 클라이언트

import twitter
import re

from javax.swing import (BoxLayout, ImageIcon, JButton, JFrame, JPanel,
        JPasswordField, JLabel, JTextArea, JTextField, JScrollPane,
        SwingConstants, WindowConstants)
from java.awt import Component, GridLayout
from java.net import URL
from java.lang import Runnable

class JyTwitter(object):
    def __init__(self):
        self.frame = JFrame("Jython Twitter",
                             defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE)


        self.loginPanel = JPanel(GridLayout(0,2))
        self.frame.add(self.loginPanel)

        self.usernameField = JTextField('',15)
        self.loginPanel.add(JLabel("username:", SwingConstants.RIGHT))
        self.loginPanel.add(self.usernameField)

        self.passwordField = JPasswordField('', 15)
        self.loginPanel.add(JLabel("password:", SwingConstants.RIGHT))
        self.loginPanel.add(self.passwordField)

        self.loginButton = JButton('Log in',actionPerformed=self.login)
        self.loginPanel.add(self.loginButton)

        self.message = JLabel("Please Log in")
        self.loginPanel.add(self.message)

        self.frame.pack()
        self.show()

    def login(self,event):
        self.message.text = "Attempting to Log in..."
        username = self.usernameField.text
        try:
            self.api = twitter.Api(username, self.passwordField.text)
            self.timeline(username)
            self.loginPanel.visible = False
            self.message.text = "Logged in"
        except:
            self.message.text = "Log in failed."
            raise
        self.frame.size = 400,800


    def timeline(self, username):
        timeline = self.api.GetFriendsTimeline(username)
        self.resultPanel = JPanel()
        self.resultPanel.layout = BoxLayout(self.resultPanel, BoxLayout.Y_AXIS)
        for s in timeline:
            self.showTweet(s)

        scrollpane = JScrollPane(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
                                 JScrollPane.HORIZONTAL_SCROLLBAR_NEVER)
        scrollpane.preferredSize = 400, 800
        scrollpane.viewport.view = self.resultPanel

        self.frame.add(scrollpane)

    def showTweet(self, status):
        user = status.user
        p = JPanel()

        p.add(JLabel(ImageIcon(URL(user.profile_image_url))))

        p.add(JTextArea(text = status.text,
                        editable = False,
                        wrapStyleWord = True,
                        lineWrap = True,
                        alignmentX = Component.LEFT_ALIGNMENT,
                        size = (300, 1)
             ))
        self.resultPanel.add(p)

    def show(self):
        self.frame.visible = True

if __name__ == '__main__':
    JyTwitter()

이 코드는 python-twitter 패키지에 의존적이다. 이 패키지는 파이썬 패키지 인덱스(PyPi)에서 찾을 수 있다. easy_install이 설치되어 있다면 python-twitter를 다음과 같이 설치할 수 있다(easy_install에 대한 지침은 부록 A 참조).

예제 16-14.

jython easy_install python-twitter

이것은 python-twitter가 의존하는 simplejson 모듈을 자동으로 설치한다. 이제 애플리케이션을 실행할 수 있을 것이다. 그림 16-3과 같은 로그인 프롬프트를 볼 수 있을 것이다.

picture_5

그림 16-3. 로그인 프롬프트

잘못된 비밀번호를 넣으면, 그림 16-4와 같은 메시지가 출력된다.

picture_6

그림 16-4. 로그인 실패

끝으로, 로그인에 성공하면 그림 16-5와 같은 것을 볼 수 있을 것이다.

picture_4

그림 16-5. 로그인 성공!

생성자는 self.frame라는 가상의 바깥 프레임을 만든다. 우리는 defaultCloseOperation을 설정했으므로 사용자가 메인 창을 닫으면 앱이 종료된다. 그 다음 우리는 사용자가 사용자 이름과 비밀번호를 입력할 수 있는 텍스트 입력란을 가진 loginPanel을 만들고, 클릭했을 때 self.login 메소드를 호출하는 로그인 버튼을 생성한다. 그 다음에 “Please log in” 라벨을 달고 프레임이 보이도록 한다.

예제 16-15.

def __init__(self):
        self.frame = JFrame("Jython Twitter",
                             defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE)

    self.loginPanel = JPanel(GridLayout(0,2))
    self.frame.add(self.loginPanel)

    self.usernameField = JTextField('',15)
    self.loginPanel.add(JLabel("username:", SwingConstants.RIGHT))
    self.loginPanel.add(self.usernameField)

    self.passwordField = JPasswordField('', 15)
    self.loginPanel.add(JLabel("password:", SwingConstants.RIGHT))
    self.loginPanel.add(self.passwordField)

    self.loginButton = JButton('Log in',actionPerformed=self.login)
    self.loginPanel.add(self.loginButton)

    self.message = JLabel("Please Log in")
    self.loginPanel.add(self.message)

    self.frame.pack()

    self.show()

로그인 메소드는 라벨 텍스트를 바꾸고 python-twitter를 호출하여 로그인을 시도한다. 뭔가 잘못되었을 때 “Log in failed”를 표시하는 것은 try/except 블록에 있다. 실제 애플리케이션은 서로 다른 유형의 예외를 확인하여 무엇이 잘못되었는지에 따라 표시하는 내용을 바꾸어줄 것이다.

예제 16-16.

def login(self,event):
    self.message.text = "Attempting to Log in..."

    username = self.usernameField.text
    try:
        self.api = twitter.Api(username, self.passwordField.text)
        self.timeline(username)
        self.loginPanel.visible = False
        self.message.text = "Logged in"
    except:
        self.message.text = "Log in failed."
        raise
    self.frame.size = 400,800

로그인이 성공하면, timeline 메소드를 호출하여, 사용자가 팔로우하는 최근의 트윗을 프레임에 채운다. timeline 메소드에서는 python-twitter API로부터 GetFriendsTimeline을 호출한다.그 다음에 상태 개체를 통해 showTweet을 매번 호출하는 것을 반복한다. 이 모든 것은 JScrollPane으로 떨어져서 적당한 크기로 설정되어, 주 프레임에 추가된다.

예제 16-17.

def timeline(self, username):
    timeline = self.api.GetFriendsTimeline(username)
    self.resultPanel = JPanel()
    self.resultPanel.layout = BoxLayout(self.resultPanel, BoxLayout.Y_AXIS)
    for s in timeline:
        self.showTweet(s)

    scrollpane = JScrollPane(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
                             JScrollPane.HORIZONTAL_SCROLLBAR_NEVER)
    scrollpane.preferredSize = 400, 800
    scrollpane.viewport.view = self.resultPanel

    self.frame.add(scrollpane)

showTweet 메소드에서 우리는 트윗을 따라가며 사용자의 아이콘(user.profile_image_url를 통해 URL을 가져온다)을 넣은 JLable과 트윗 내용을 담는 JTextArea를 붙여 나간다. JTextArea를 올바르게 표시하기 위해서 설정해야 하는 모든 bean 속성을 확인하라.

예제 16-18.

def showTweet(self, status):
    user = status.user
    p = JPanel()


    p.add(JLabel(ImageIcon(URL(user.profile_image_url))))

    p.add(JTextArea(text = status.text,
                    editable = False,
                    wrapStyleWord = True,
                    lineWrap = True,
                    alignmentX = Component.LEFT_ALIGNMENT,
                    size = (300, 1)
         ))
    self.resultPanel.add(p)

요약

이것으로 자이썬으로 만든 스윙 GUI 맛보기를 마치도록 하겠다. 다시 말하지만, 스윙은 매우 방대한 주제이므로, 제대로 다루기 위해서는 스윙에 대한 전문적인 자료를 찾아보는 것이 좋다. 이 장의 내용을 습득하였다면, 자바 스윙 예제를 자이썬 스윙 예제로 충분히 변환할 수 있을 것이다.