第 16 章:GUI 应用程序

Python 的 C 实现附带 Tcl/TK 用于编写图形用户界面 (GUI)。在 Jython 上,您自动获得的 GUI 工具包是 Swing,它随 Java 平台一起提供。与 CPython 一样,还有其他工具包可用于在 Jython 中编写 GUI。由于 Swing 在任何现代 Java 安装中都可用,因此本章将重点介绍在 Jython 中使用 Swing GUI。

Swing 是一个庞大的主题,无法在一章中完全涵盖。事实上,有整本书专门介绍这个主题。我将对 Swing 做一些介绍,但仅限于描述从 Jython 使用 Swing 的方法。要深入了解 Swing,应该使用众多书籍或网络教程之一。[FJW:一些建议的书籍/教程?]。

从 Jython 使用 Swing 比在 Java 中使用 Swing 有许多优势。例如,在 Jython 中,bean 属性的冗长性较低,并且在 Jython 中绑定操作的冗长性也低得多(在 Java 中通常使用匿名类,在 Jython 中可以传递函数)。

让我们从一个简单的 Java Swing 应用程序开始,然后我们将看看 Jython 中的相同应用程序。

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);
    }
}

这个简单的应用程序绘制一个 JFrame,该 JFrame 完全由一个 JButton 填充。按下按钮时,命令行上会打印出“Clicked!”。

../_images/chapter17-hello-java.jpg

现在让我们看看这个程序在 Jython 中的样子

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,并在单击按钮时输出“Clicked!”。

../_images/chapter17-hello-jython.jpg

让我们逐行浏览 Java 和 Jython 示例,以了解在 Jython 和 Java 中编写 Swing 应用程序之间的区别。首先是导入语句

在 Java 中

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

在 Jython 中

from javax.swing import JButton, JFrame

在 Jython 中,最好始终按名称显式导入,而不是使用

from javax.swing import *

出于第 7 章中介绍的原因。请注意,我们不需要导入 ActionEvent 或 ActionListener,因为 Jython 的动态类型允许我们在代码中避免提及这些类。

接下来,我们有一些代码来创建一个 JFrame,然后设置几个 bean 属性。

在 Java 中:
JFrame frame = new JFrame(“Hello Java!”); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(300, 300);

在 Jython 中

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

在 Java 中,首先创建一个新的 JFrame,然后设置 bean 属性 defaultCloseOperation 和 size。在 Jython 中,我们可以在调用构造函数时直接添加 bean 属性设置器。这种快捷方式在第 [FJW] 章中已经详细介绍过了。不过,这里还是需要重复一下,因为 bean 属性在 Swing 库中非常重要。简而言之,如果你有一个 getFoo/setFoo 形式的 getter 和 setter,你可以将它们视为名为“foo”的对象属性。因此,你可以使用 x.foo 代替 x.getFoo()。你可以使用 x.foo = bar 代替 x.setFoo(bar)。如果你看一下任何规模合理的 Swing 应用程序,你很可能会看到一大块像这样的设置器

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

在我看来,在 Jython 的惯用属性设置风格中,这些代码看起来更好

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,并将按钮绑定到一个打印“Clicked!”的动作。

在 Java 中

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

在 Jython 中

def change_text(event):
    print 'Clicked!'

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

我认为 Jython 的方法在这里比 Java 更好。在这里,我们可以将一个一等函数“change_text”直接传递给 JButton 的构造函数,而不是使用更繁琐的 Java“addActionListener”方法,在该方法中,我们需要创建一个匿名 ActionListener 类并定义它的 actionPerfomed 方法,并包含所有静态类型声明的仪式。这是一种 Jython 的可读性真正突出的情况。最后,在两个示例中,我们都将可见属性设置为 True。同样,虽然我们可以在框架构造函数中设置此属性,但可见属性是那些罕见的依赖于顺序的属性之一,我们希望在正确的时间(在本例中,最后)设置它。

在 Java 中

frame.setVisible(true);

在 Jython 中

frame.visible = True

现在我们已经看过了简单的示例,了解一个中等规模的应用程序在 Jython 中可能是什么样子是有意义的。由于 Twitter 应用程序已经成为当今 GUI 应用程序的“Hello World”,我们将遵循这种趋势。以下应用程序会向用户显示一个登录提示。当用户成功登录后,其时间线上的最新推文将显示出来。以下是代码

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")
        self.frame.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.frame.visible = True

    def login(self,event):
        self.message.text = "Attempting to Log in..."
        self.frame.show()
        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
        self.frame.show()

    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()

        # image grabbing seems very expensive, good place for a callback?
        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)

if __name__ == '__main__':
    JyTwitter()

此代码依赖于 python-twitter 包。此包可以在 Python 包索引 (PyPi) 上找到。如果你有 easy_install(有关 easy_install 的说明,请参见第 ? 章),那么你可以像这样安装 python-twitter

jython easy_install python-twitter

这将自动安装 python-twitter 的依赖项:simplejson。现在你应该能够运行应用程序。你应该看到以下登录提示

../_images/chapter17-login.jpg

如果你输入了错误的密码,你应该看到

../_images/chapter17-failed-login.jpg

最后,当你成功登录后,你应该看到类似以下内容

../_images/chapter17-twitter.jpg

构造函数创建了外部框架,想象一下它被命名为 self.frame。我们设置了 defaultCloseOperation,以便当用户关闭主窗口时应用程序将终止。然后我们创建一个 loginPanel,它包含用于用户输入用户名和密码的文本字段,并创建一个登录按钮,当单击该按钮时,它将调用 self.login 方法。然后我们在其中放一个“请登录”标签,并使框架可见。

def __init__(self):
    self.frame = JFrame("Jython Twitter")
    self.frame.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.frame.visible = True

login 方法更改标签文本并调用 python-twitter 尝试登录。它位于一个 try/excpet 块中,如果出现错误,该块将显示“登录失败”。一个真正的应用程序将检查不同类型的异常,以查看发生了什么错误,并相应地更改显示消息。

def login(self,event):
    self.message.text = "Attempting to Log in..."
    self.frame.show()
    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
    self.frame.show()

如果登录成功,我们将调用 timeline 方法,该方法使用用户正在关注的最新推文填充框架。在 timeline 方法中,我们从 python-twitter API 调用 GetFriendsTimeline,然后我们遍历状态对象并对每个对象调用 showTweet。所有这些都被放入一个 JScrollPane 中,并设置为一个合理的大小,然后添加到主框架中。

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)的 JLabel 和一个包含推文文本的 JTextArea。请注意,我们设置的所有 bean 属性都使 JTextArea 正确显示。

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

    # image grabbing seems very expensive, good place for a callback?
    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)

这结束了我们对 Jython 中 Swing 的快速浏览。再次强调,Swing 是一个非常庞大的主题,因此您需要参考一些专门的 Swing 资源才能真正掌握它。在本章之后,将 Java 示例转换为 Jython 示例应该相当简单。