Graphical User Interface

GUI 中,并不只是 键入文本和返回文本,用户可以看到窗口、按钮、文本框等图形,而且可以用鼠标点击,还可以通过键盘键入。我们目前为止完成的程序都是命令行或文本模式程序。GUI 是与程序交互的一种不同的方式。有 GUI 的程序仍然有 3 个基本要素:输入、处理和输出,但它们的输入和输出更丰富、更有趣一些。

EasyGui 是一个 Python 模块,利用这个模块可以很容易地建立简单的 GUI。模块就是一种扩展方法,通过它可以向 Python 增加非内置的内容。

EasyGui.msgbox() 函数用于创建一个消息框。大多数情况下,EasyGui 函数的名就是相应英语单词的缩写。

  1. >>> import easygui
  2. >>> easygui.msgbox("Hello There!")
  3. 'OK'

image.png

GUI 输入

'OK' 部分就是 PythonEasyGui 在告诉你:用户点击了 OK 按钮。EasyGui 会返回信息来告诉你用户在 GUI 中做了什么:点击了什么按钮,键入了哪些内容等等。可以为这个响应指定一个名字(把它赋给一个变量)。

  1. >>> user_response = easygui.msgbox("Hello there!")
  2. >>> print(user_response)
  3. OK

现在用户的响应(OK)有了一个变量名 user_response

消息框实际上只是对话框(dialog box)的一个例子。对话框包含一些 GUI 元素,用来告诉用户某些信息,或者从用户得到一些输入。输入可以是按钮点击(如 OK),或者文件名,也可以是某个文本(字符串)。

EasyGui.msgbox 就是包含一条消息和一个OK按钮的对话框。不过还可以有不同类型的对话框,包含更多的按钮和其他内容。

多个按钮

下面来创建一个包含多个按钮的对话框(如消息框)。具体做法是使用一个按钮框(button box,buttonbox)。

  1. import easygui
  2. title = "What is your favorite ice cream flavor?"
  3. options = ['Vanilla', 'Chocolate', 'Strawberry']
  4. flavor = easygui.buttonbox(title, choices = options)
  5. easygui.msgbox("You picked " + flavor)

image.png

选择框

EasyGui 提供了一种选择框(choice box, choicebox),它会显示一个选择列表。用户可以选择其中之一,然后点击 OK 按钮。

  1. import easygui
  2. title = "What is your favorite ice cream flavor?"
  3. options = ['Vanilla', 'Chocolate', 'Strawberry']
  4. flavor = easygui.choicebox(title, choices = options)
  5. easygui.msgbox("You picked " + flavor)

image.png
选择一个口味然后点击 OK 时,你会看到与前面相同的消息框。注意,除了用鼠标点击选择,还可以用键盘上的上下箭头键选择一个口味。

文本输入

如果想像 input() 一样(也就是让用户键入文本),该怎么做呢?这样用户就可以输入自己喜欢的任何口味了。EasyGui 提供了一种输入框(enter box ,enterbox)能够做到这一点。

  1. import easygui
  2. flavor = easygui.enterbox("What is your favorite ice cream flavor?")
  3. easygui.msgbox ("You entered " + flavor)

image.png
这就类似于 input(),同样可以从用户得到文本(一个字符串)。

默认输入

有时用户输入信息时,可能会期望得到某个答案,或者有一个很常见或最可能输人的答案。这称为默认值(default)。这个最常见的答案可以由你为用户自动输人,这样用户就不用再键入了。有了默认值,只有当用户有不同的输入时才有必要键入。

  1. import easygui
  2. flavor = easygui.enterbox("What is your favorite ice cream flavor?", default = 'Vanilla')
  3. easygui.msgbox ("You entered " + flavor)

image.png
现在运行这个程序时,输人框中已经输入了“Vanilla”(香草)。可以把它删掉,再输入你想要的内容,不过如果你最喜欢的口味确实是香草,就不用再键人任何内容,只需点击0K。

数字

如果想在EasyGui中输入一个数,完全可以先通过输人框得到一个字符串,然后使用int()或者float()由这个字符串创建一个数。

EasyGui 还提供了一种整数框(integer box ,integerbox),可以用它来输入整数。还可以对所输入的数设置一个下界和上界。

不过,整数框不允许输入浮点数(小数)。要输入小数,必须先通过输入框得到字符串,然后再使用 float() 转换这个字符串。

猜数字游戏

我们之前创建了一个简单的猜数程序。下面再来完成这个程序,不过这一次要使用 EasyGui 完成输入和输出。

  1. #!/usr/local/bin/env python3
  2. # encoding: utf-8
  3. import random
  4. import easygui
  5. secret = random.randint(1, 99)
  6. guess = 0
  7. tried = 0
  8. easygui.msgbox("AHOY! I'm the Dread Pirate Roberts, and I have a secret! It is a number from 1 to 99. I'll give you 6 tries.")
  9. while guess != secret and tried < 6:
  10. guess = easygui.integerbox("What's your guess? ")
  11. if guess < secret:
  12. easygui.msgbox("Too low, you lazy dog!")
  13. elif guess > secret:
  14. easygui.msgbox ("Too high, landlubber!")
  15. tried = tried + 1
  16. if guess == secret:
  17. easygui.msgbox("Yeah! You got it! Found my secret, you did it!")
  18. else:
  19. easygui.msgbox("No more guesses! Better luck next time, boy!")
  20. easygui.msgbox("The secret number was " + str(secret))

image.png
image.png
EasyGui 还提供了另外一些 GUI 组件,包括允许多重选择(而不是只选择一项)的选择框,还有一些特殊的对话框用来得到文件名等内容。不过,对现在来说,前面介绍的 GUI 组件已经足够了。

利用 EasyGui,我们可以非常容易地生成一些简单的 GUI,而且它隐藏了 GUI 涉及的很多复杂性,使你不用再操心这些问题。后面我们将会讨论建立 GUI 的另一种方法,它可以提供更多的灵活性和控制。

help()

Python 提供了一个内置的帮助系统,在交互模式中,可以在交互提示符后面键入 help(),就会进入这个帮助系统。现在提示符会变成 help >

  1. >>>he1p ()
  2. help >

一旦进入帮助系统,你想要得到哪方面的帮助,只需要键入相应的名字,例如:help> easygui.msgbox 你就会得到你想要的一些信息。
image.png

要退出帮助系统,重回正常的交互提示符,只需要键入 quit

  1. help> quit
  2. >>>

Revision

  • 如何利用 EasyGui 建立简单的GUI
  • 如何使用消息框 msgbox 显示消息。
  • 如何使用按钮、选择框和文本输入框(buttonboxchoiceboxenterboxintegerbox)得到输入。
  • 如何为一个文本框设置默认输入。
  • ˆˆ如何使用 Python 的内置帮助系统。

测试题

  1. 如何使用 EasyGui 生成消息框?
  2. 如何使用 EasyGui 得到字符串(一些文本)输入?
  3. 如何使用 EasyGui 得到整数输入?
  4. 如何使用 EasyGui 得到浮点数(小数)输入?
  5. 什么是默认值?给出一个可能使用默认值的例子。

动手试一试

  1. 试着修改温度转换程序,这一次要用 GUI 输入和输出而不是 input()print
  2. 编写一个程序,询问你的姓名,然后是房间号、街道和城市,接下来是省/地区/州,最后是邮政编码(所有这些都放在 EasyGui 对话框中)。然后这个程序要显示一个寄信格式的完整地址,类似于:
    1. John Snead
    2. 28 Main Street
    3. Akron, Ohio
    4. 12345

Skier

学习编程有一种惯常的做法,就是先键入一些代码,然后运行它,尽管你可能完全不理解这些代码,没有关系!

有时仅仅键入代码就能让你对程序如何工作找到一点“感觉”,虽然并不是每一行或每一个关键字都理解。我们在第 1 章就是这么做的,就是那个猜数游戏。现在还是用这个老办法建立一个程序,不过这个程序更长也更有意思。

Skier(滑雪的人)是一个非常简单的滑雪游戏,在这个游戏中,你要滑下小山,努力避开树而且要尽量捡起小旗。捡起一个小旗得 10 分;碰到树则会丢掉 100 分。Skier 使用一个名叫 Pygame 的模块来帮助实现图形。

  1. import pygame
  2. import sys
  3. import random
  4. skier_images = ["skier_down.png", "skier_right1.png", "skier_right2.png", "skier_left2.png", "skier_left1.png"]
  5. #This creates the skier
  6. class SkierClass(pygame.sprite.Sprite):
  7. def __init__(self):
  8. pygame.sprite.Sprite.__init__(self)
  9. self.image = pygame.image.load("skier_down.png")
  10. self.rect = self.image.get_rect()
  11. self.rect.center = [320, 100]
  12. self.angle = 0
  13. def turn(self, direction): #the skier turn around
  14. self.angle = self.angle + direction
  15. if self.angle < -2: self.angle = -2
  16. if self.angle > 2: self.angle = 2
  17. center = self.rect.center
  18. self.image = pygame.image.load(skier_images[self.angle])
  19. self.rect = self.image.get_rect()
  20. self.rect.center = center
  21. speed = [self.angle, 6 - abs(self.angle)* 2]
  22. return speed
  23. def move(self, speed): #the skier move on
  24. self.rect.centerx = self.rect.centerx + speed[0]
  25. if self.rect.centerx < 20: self.rect.centerx = 20
  26. if self.rect.centerx > 620: self.rect.centerx = 620
  27. #the obstacle: tree or flag
  28. class ObstacleClass(pygame.sprite.Sprite):
  29. def __init__(self, image_file, location, type):
  30. pygame.sprite.Sprite.__init__(self)
  31. self.image_file = image_file
  32. self.image = pygame.image.load(image_file)
  33. self.location = location
  34. self.rect = self.image.get_rect()
  35. self.rect.center = location
  36. self.type = type
  37. self.passed = False
  38. def scroll(self, t_ptr): #moving the obstacle
  39. self.rect.centery = self.location[1] - t_ptr
  40. def create_map(start, end): #the game map
  41. obstacles = pygame.sprite.Group()
  42. gates = pygame.sprite.Group()
  43. locations = []
  44. for i in range(10):
  45. row = random.randint(start, end)
  46. col = random.randint(0, 9)
  47. location = [col * 64 + 20, row * 64 + 20]
  48. if not (location in locations):
  49. locations.append(location)
  50. type = random.choice(["tree", "flag"])
  51. if type == "tree": img = "skier_tree.png"
  52. elif type == "flag":img = "skier_flag.png"
  53. obstacle = ObstacleClass(img, location, type)
  54. obstacles.add(obstacle)
  55. return obstacles
  56. def animate(): #draw on screen
  57. screen.fill([255, 255, 255])
  58. pygame.display.update(obstacles.draw(screen))
  59. screen.blit(skier.image, skier.rect)
  60. screen.blit(score_text, [10, 10])
  61. pygame.display.flip()
  62. def updateObstacleGroup(map0, map1):
  63. obstacles = pygame.sprite.Group()
  64. for ob in map0: obstacles.add(ob)
  65. for ob in map1: obstacles.add(ob)
  66. return obstacles
  67. pygame.init()
  68. screen = pygame.display.set_mode([640,640])
  69. clock = pygame.time.Clock()
  70. skier = SkierClass()
  71. print("created a skier")
  72. speed = [0, 6]
  73. map_position = 0
  74. points = 0
  75. map0 = create_map(20, 29)
  76. map1 = create_map(10, 19)
  77. activeMap = 0
  78. obstacles = updateObstacleGroup(map0, map1)
  79. font = pygame.font.Font(None, 50)
  80. while True:
  81. print ("we are in the game loop")
  82. clock.tick(30)
  83. for event in pygame.event.get():
  84. print("event loop")
  85. if event.type == pygame.QUIT: sys.exit()
  86. if event.type == pygame.KEYDOWN:
  87. if event.key == pygame.K_LEFT:
  88. speed = skier.turn(-1)
  89. elif event.key == pygame.K_RIGHT:
  90. speed = skier.turn(1)
  91. skier.move(speed)
  92. map_position += speed[1]
  93. if map_position >=640 and activeMap == 0:
  94. activeMap = 1
  95. map0 = create_map(20, 29)
  96. obstacles = updateObstacleGroup(map0, map1)
  97. if map_position >=1280 and activeMap == 1:
  98. activeMap = 0
  99. for ob in map0:
  100. ob.location[1] = ob.location[1] - 1280
  101. map_position = map_position -1280
  102. map1 = create_map(10,19)
  103. obstacles = updateObstacleGroup(map0, map1)
  104. for obstacle in obstacles:
  105. obstacle.scroll(map_position)
  106. hit = pygame.sprite.spritecollide(skier, obstacles, False)
  107. if hit:
  108. if hit[0].type == "tree" and not hit[0].passed:
  109. point = points - 100
  110. skier.image = pygame.image.load("skier_crash.png")
  111. animate()
  112. pygame.time.delay(1000)
  113. skier.image = pygame.image.load("skier_down.png")
  114. skier.angle = 0
  115. speed = (6, 0)
  116. hit[0].passed = True
  117. elif hit[0].type == "flag" and not hit[0].passed:
  118. points += 10
  119. obstacles.remove(hit[0])
  120. score_text = font.render("score: " + str(points), 1, (0, 0, 0))
  121. animate()
  122. pygame.quit()

image.png

动手试一试

这一章你要做的只是键入这个 Skier 程序,再运行试试看。如果运行时遇到错误,看看错误消息,试着找出错误究竟出现在哪里。

祝你好运!

Skier 进阶

滑雪者

当运行 Skier 程序的时候,应该注意到了滑雪者本身只能在屏幕上左右来回移动,而不能上下移动。滑雪者滑“下”小山的错觉则是通过将场景(树和小旗)向上滚动来实现的。

方向

在滑雪者滑下小山的场景实现中,需要 5 张不同的图片:一张滑雪者一直向下滑,两张滑雪者向左转(一张稍向左转,一张大幅度左转),两张滑雪者向右转(一张稍向右转,一张大幅度右转)。在程序的开头,我们为这些图片创建了一个列表,然后将图片按特定的顺序放入列表。

  1. skier_images = [ "skier_down.png", "skier_right1.png","skier_right2.png",
  2. "skier_left2.png","skier_left1.png"]

使用变量 angle 来标记滑雪者当前面对的方向,它的值从 -2 到 +2,分别如下:

  • ˆˆ-2 = 向左急转 ˆˆ
  • -1 = 稍向左转
  • 0 = 一直向下 ˆˆ
  • +1 = 稍向右转
  • ˆˆ+2 = 向右急转

注意,这里的“左”和“右”是相对屏幕的方向,即我们看到的方向,而不是 滑雪者的左和右。

使用 angle 的值来确定要使用哪张图片。我们可以直接使用 angle 的值作为列表的索引来指定图片:

  • ˆˆskier_images[0] 是滑雪者向下滑的图片。
  • ˆˆskier_images[1] 是滑雪者稍向右转的图片。
  • ˆˆskier_images[2] 是滑雪者向右急转的图片。
  • skier_images[-1] 是滑雪者稍向左转的图片(通常会使用 skier_images[4])。
  • ˆˆskier_images[-2] 是滑雪者向左急转的图片(通常会使用 skier_images[3])。

转弯

用一个类来改变滑雪者的状态,它会在 angle 的值改变时载入正确的图片,并设置好滑雪者的速度。速度由 xy 两个值构成。我们只改变左右方向的速度(x 方向的速度,x-speed),但是 y 方向的速度(y-speed)决定了场景向上滚动的速度(滑雪者向“下”滑的速度)。当他垂直向下运动时,y 方向的速度比较快,而当他转向时,y 方向的速度则比较慢。速度的计算公式如下:

  1. speed = [self.angle, 6 - abs(self.angle) * 2]

此行代码中的 abs 用于取得 angle 的绝对值,也就是说我们忽略符号(+ 和 -)后的值。对于 y 方向的速度来说,滑雪者左转还是右转都没有影响,只要知道转的程度就行了。

移动

使用方向键来控制滑雪者左右运动,所以要添加 Pygame 初始化和事件循环的代码,这样就可以让只有滑雪者的程序运行起来。

障碍物

创建了一个名为 ObstacleClass 的类。和滑雪者一样,这也是一个 Pygame Sprite 类。

地图

不要尝试将两个障碍物放在同一位置,所以我们需要知道哪些位置是已经使用过的。变量 locations 是一个用于记录使用过的位置的列表。当想要在某个位置放置一个新的障碍物时,我们首先要查看这个位置是否已有一个障碍物。

animate() 函数来重绘屏幕。

在障碍物类的 update() 方法中,添加一个判断逻辑,看看障碍物是否已经移出屏幕。如果是的话,就移除它。Pygame 有一个名为 kill() 的原 生方法可用来做这件事。

现在可以将滑雪者和障碍物的代码放到一起了:

  • ˆˆ需要 SkierClassObstacleClass
  • ˆˆanimate() 函数需要同时绘制滑雪者和障碍物。
  • ˆˆ初始化代码需要创建滑雪者和初始地图。
  • ˆˆ主循环需要同时包括滑雪者的键盘事件绑定和障碍物场景的创建。

最后需要处理的两项是:

  1. ˆˆ检测滑雪者是否碰到了树或者是否捡到了小旗
  2. ˆˆ记录并显示分数

直接用 spritecollide() 函数来检测滑雪者是否碰到树或者小旗。接下来需要知道这个障碍物是什么(树还是小旗),然后:

  1. ˆˆ如果是树,则将滑雪者的图像切换为“碰撞”的图像,并将分数减去 100。
  2. ˆˆ如果是小旗,则将分数加 10,然后将小旗从屏幕中移除。

变量 hit 告诉我们滑雪者撞到了哪个障碍物。它是一个列表,但在这里列表中只有一个条目,因为滑雪者一次只可能碰到一个障碍物,所以他碰到的障碍物是 hit[0]。

passed 变量用于标记是否碰撞过树。这能保证当滑雪者在撞树之后继续往下滑时,不会立刻运行撞到同一棵树的逻辑。

现在需要显示分数。在主循环中,使用一个新的分数文本来渲染 font 对象。animate() 函数中,在左上角显示分数:

  1. font = pygame.font.Font(None, 50)
  2. score_text = font.render("Score: " + str(points), 1, (0, 0, 0))
  3. screen.blit(score_text, [10, 10])

Revision

  • Skier 游戏的各个部分是如何工作的
  • ˆˆ怎样创建一个滚动的背景

动手试一试

  1. 尝试修改 Skier 游戏,使游戏的难度随着游戏的进行逐渐增大。可以尝试以下建议:
  • ˆˆ使游戏的速度随着游戏的进行逐渐加快。
  • ˆˆ滑雪者滑下小山时树越来越多。
  • ˆˆ添加障碍物“冰”,使得滑雪者转向时更加困难。
  1. 在个游戏中增加一个讨厌的雪人,它会随机出现并追赶滑雪者。你需要找到或者创建一个新的图片,并且修改代码以获得你想要的行为。