Kivy Python 交互式应用和游戏指南(二)

原文:zh.annas-archive.org/md5/6f1edc8ae20cbffd0b2f654cff980f50

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章。侵略者复仇 - 一个交互式多点触控游戏

本章介绍了一系列组件和策略,用于制作动画和动态应用程序。其中大部分特别适用于游戏开发。本章充满了如何结合不同的 Kivy 元素以及控制同时发生的多个事件的策略的示例。所有示例都集成在一个全新的项目中,这是一个经典游戏《太空侵略者》的版本(版权©1978 年太田公司,en.wikipedia.org/wiki/Space_Invaders)。以下是我们将在本章中工作的主要组件列表:

  • 图集:一个 Kivy 包,允许我们高效地加载图像

  • 声音:允许声音管理的类

  • 动画:可以应用于小部件的过渡、时间控制、事件和操作

  • 时钟:一个允许我们安排事件的类

  • 多点触控:一种允许我们根据触摸控制不同动作的策略

  • 键盘:Kivy 捕获键盘事件的策略

第一部分介绍了项目概述、GUI 和游戏规则。之后,我们将采用自下而上的方法。解释与游戏各个组件相关的简单类,然后依次介绍本章的其他主题。我们将以对游戏有主要控制权的类结束。到本章结束时,你应该能够开始为你的移动设备实现任何你一直想要实现的游戏应用程序。

侵略者复仇 - 一个动画多点触控游戏

侵略者复仇是我们 Kivy 版本的《太空侵略者》©的名称。以下截图显示了本章我们将构建的游戏:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_05_01.jpg

截图中有几个黄色和青色的标签(或打印版本中的灰色虚线)。它们帮助我们识别游戏的结构;游戏将包括一个射击者(玩家),他向 32(8x4)个侵略者射击(射击),这些侵略者试图用他们的导弹摧毁射击者侵略者组织在一个舰队(水平移动)中,有时一个单独的侵略者可以突破网格结构,在屏幕上飞来飞去,然后再回到其在舰队中的对应位置(码头)。

屏幕上横跨的青色(打印版本中为灰色)线条表示屏幕内部将屏幕划分为敌对区域射击区域。这种划分用于区分根据屏幕不同部分发生的触摸而应发生的动作。

游戏的骨架在invasion.kv文件中展示:

1\. # File name: invasion.kv
2\. <Invasion>:
3\.   id: _invasion
4\.   shooter: _shooter
5\.   fleet: _fleet
6\.   AnchorLayout:
7\.     anchor_y: 'top'
8\.     anchor_x: 'center'
9\.     FloatLayout:
10\.       id: _enemy_area
11\.       size_hint: 1, .7
12\.       Fleet:
13\.         id: _fleet
14\.         invasion: _invasion
15\.         shooter: _shooter
16\.         cols: 8
17\.         spacing: 40
18\.         size_hint: .5, .4
19\.         pos_hint: {'top': .9}
20\.         x: root.width/2-root.width/4
21\.   AnchorLayout:
22\.     anchor_y: 'bottom'
23\.     anchor_x: 'center'
24\.     FloatLayout:
25\.       size_hint: 1, .3
26\.       Shooter:
27\.         id: _shooter
28\.         invasion: _invasion
29\.         enemy_area: _enemy_area

有两个AnchorLayout实例。上面的一个是包含舰队敌对区域,下面的一个是包含射击者射击区域

小贴士

敌人区域射击区域对于游戏的逻辑非常重要,以便区分屏幕上触摸的类型。

我们还创建了一些 ID 和引用,这将允许不同界面实例之间的交互。以下图表总结了这些关系:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_05_02.jpg

Atlas – 高效的图像管理

当涉及到使用许多图像的应用程序时,减少它们的加载时间非常重要,尤其是在它们从远程服务器请求时。

注意

减少加载时间的一种策略是使用Atlas(也称为精灵)。Atlas 将所有应用程序图像组合成一个大的图像,因此减少了必要的操作系统或在线请求的数量。

这里是我们用于“侵略者复仇”游戏的 Atlas 图像:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_05_03.jpg

我们将不再请求五个“侵略者复仇”图像,而是只请求 Atlas 图像。我们还需要一个相关的json文件,它告诉我们图像中每个单元的确切坐标。好消息是,我们不需要手动做这件事。Kivy 提供了一个简单的命令来创建 Atlas 图像和json文件。假设所有图像都在名为img的目录中,我们只需要打开终端,转到img目录(包含单个图像),然后在终端中运行以下命令:

python -m kivy.atlas invasion 100 *.png

注意

为了执行前面的命令,您需要安装Pillow 库(python-pillow.github.io/)。

命令包含三个参数,即**basenamesizeimages listbasename参数是json文件(img/invasion.json)和 Atlas 图像或图像(img/invasion-0.png)的前缀。可能会生成多个 Atlas 图像,在这种情况下,我们会有一系列以basename为前缀并跟随数字标识符的图像,例如,invasion-0.pnginvasion-1.pngsize参数表示结果 Atlas 图像的像素大小。请确保指定一个比最大的图像更大的大小。image list**参数是要添加到 Atlas 的所有图像的列表,我们可以使用*通配符。在我们的情况下,我们将使用它来指示所有具有.png扩展名的文件。

为了在 Kivy 语言中使用 Atlas,我们必须使用以下格式:atlas://path/to/atlas/atlas_name/idid文件指的是不带扩展名的图像文件名。例如,我们通常会将射击者图像作为源引用:'img/shooter.png'。在生成 Atlas 后,它变为source: 'atlas://images/invasion/shooter'。下面的image.kv文件展示了“侵略者复仇”中所有图像的代码:

30\. # File name: images.kv
31\. <Invader>:
32\.   source: 'atlas://img/invasion/invader'
33\.   size_hint: None,None
34\.   size: 40,40
35\. <Shooter>:
36\.   source: 'atlas://img/invasion/shooter'
37\.   size_hint: None,None
38\.   size: 40,40
39\.   pos: self.parent.width/2, 0
40\. <Boom>:
41\.   source: 'atlas://img/invasion/boom'
42\.   size_hint: None,None
43\.   size: 26,30
44\. <Shot>:
45\.   source: 'atlas://img/invasion/shot'
46\.   size_hint: None,None
47\.   size: 12,15
48\. <Missile>:
49\.   source: 'atlas://img/invasion/missile'
50\.   size_hint: None,None
51\.   size: 12,27

本文件中的所有类都是直接或间接地从 Image 类继承而来的。MissileShot 首先从名为 Ammo 的类继承,该类也继承自 Image。还有一个 Boom 类,当任何 Ammo 被触发时,它将创建爆炸效果。除了 Boom 图像(Atlas 中的星星)外,Boom 类还将与我们在下一节中添加的声音相关联。

爆炸声 – 简单的声音效果

在 Kivy 中添加声音效果非常简单。当创建 Boom 实例时,它会产生声音,并且每次发射 射击导弹 时都会发生这种情况。以下是 boom.py 的代码:

52\. # File name: boom.py
53\. from kivy.uix.image import Image
54\. from kivy.core.audio import SoundLoader
55\. 
56\. class Boom(Image):
57\.   sound = SoundLoader.load('boom.wav')
58\.   def boom(self, **kwargs):
59\.     self.__class__.sound.play()
60\.     super(Boom, self).__init__(**kwargs)

生成声音涉及使用两个类,SoundSoundLoader(第 54 行)。SoundLoader 加载音频文件(.wav)并返回一个 Sound 实例(第 57 行),我们将其保存在 sound 引用中(Boom 类的静态属性)。每当创建一个新的 Boom 实例时,我们都会播放声音。

Ammo – 简单动画

本节解释了如何对 射击导弹 进行动画处理,它们表现出非常相似的行为。它们从原始位置移动到目的地,不断检查是否击中了目标。以下是对 ammo.py 类的代码:

61\. # File name: ammo.py
62\. from kivy.animation import Animation
63\. from kivy.uix.image import Image
64\. from boom import Boom
65\. 
66\. class Ammo(Image):
67\.   def shoot(self, tx, ty, target):
68\.     self.target = target
69\.     self.animation = Animation(x=tx, top=ty)
70\.     self.animation.bind(on_start = self.on_start)
71\.     self.animation.bind(on_progress = self.on_progress)
72\.     self.animation.bind(on_complete = self.on_stop)
73\.     self.animation.start(self)
74\. 
75\.   def on_start(self, instance, value):
76\.     self.boom = Boom()
77\.     self.boom.center=self.center
78\.     self.parent.add_widget(self.boom)
79\. 
80\.   def on_progress(self, instance, value, progression):
81\.     if progression >= .1:
82\.       self.parent.remove_widget(self.boom)
83\.     if self.target.collide_ammo(self):
84\.       self.animation.stop(self)
85\. 
86\.   def on_stop(self, instance,value):
87\.     self.parent.remove_widget(self)
88\. 
89\. class Shot(Ammo):
90\.   pass
91\. class Missile(Ammo):
92\.   pass

对于 Ammo 动画,我们需要一个简单的 Animation(第 69 行)。我们发送 xtop 作为参数。

注意

Animation 实例的参数可以是应用动画的部件的任何属性。

在这种情况下,xtop 属性属于 Ammo 本身。这足以将 AmmoAnimation 从其原始位置设置为 txty

注意

默认情况下,Animation 的执行周期为 1 秒。

Ammo 的轨迹中,我们需要 Ammo 做一些额外的事情。

注意

Animation 类包括三个事件,当动画开始时触发(on_start),在其进行过程中触发(on_progress),以及当它停止时触发(on_stop)。

我们将这些事件(第 70 至 72 行)绑定到我们自己的方法上。on_start 方法(第 75 行)在动画开始时显示一个 Boom 实例(第 76 行)。on_progress(第 80 至 84 行)方法在 10% 的 progression(第 81 和 82 行)后移除 Boom。此外,它还会不断检查 target(第 83 行)。当 target 被击中时,动画停止(第 84 行)。一旦动画结束(或被停止),Ammo 就会从父级中移除(第 82 行)。

第 89 至 92 行定义了两个类,ShotMissileShotMissile 类从 Ammo 继承,它们目前唯一的区别是 images.kv 中使用的图像。最终,我们将使用 Shot 实例进行 射击,使用 Missile 实例进行 入侵者。在此之前,让我们给 入侵者 一些自由,这样它们就可以离开它们的 舰队 并执行单独的攻击。

入侵者 – 动画的过渡

前一节使用默认的Animation过渡。这是一个Linear过渡,这意味着Widget实例从一个点移动到另一个点是一条直线。入侵者的轨迹可以更有趣。例如,可能会有加速度或方向变化,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_05_04.jpg

以下为invader.py的代码:

93\. # File name: invader.py
94\. from kivy.core.window import Window
95\. from kivy.uix.image import Image
96\. from kivy.animation import Animation
97\. from random import choice, randint
98\. from ammo import Missile
99\. 
100\. class Invader(Image):
101\.   pre_fix = ['in_','out_','in_out_']
102\.   functions = ['back','bounce','circ','cubic',
103\.     'elastic','expo','quad','quart','quint','sine']
104\.   formation = True
105\. 
106\.   def solo_attack(self):
107\.     if self.formation:
108\.       self.parent.unbind_invader()
109\.       animation = self.trajectory()
110\.       animation.bind(on_complete = self.to_dock)
111\.       animation.start(self)
112\. 
113\.   def trajectory(self):
114\.     fleet = self.parent.parent
115\.     area = fleet.parent
116\.     x = choice((-self.width,area.width+self.width))
117\.     y = randint(round(area.y), round(fleet.y))
118\.     t = choice(self.pre_fix) + choice(self.functions)
119\.     return Animation(x=x, y=y,d=randint(2,7),t=t)
120\. 
121\.   def to_dock(self, instance, value):
122\.     self.y = Window.height
123\.     self.center_x = Window.width/2
124\.     animation = Animation(pos=self.parent.pos, d=2)
125\.     animation.bind(on_complete = self.parent.bind_invader)
126\.     animation.start(self)
127\. 
128\.   def drop_missile(self):
129\.     missile = Missile()
130\.     missile.center = (self.center_x, self.y)
131\.     fleet = self.parent.parent
132\.     fleet.invasion.add_widget(missile)
133\.     missile.shoot(self.center_x,0,fleet.shooter)

这段代码背后的想法是让一个入侵者舰队中打破队形并进入solo_attack(第 106 至 111 行)方法。入侵者Animationtrajectory方法(第 113 和 119 行)中创建,通过随机化入侵者轨迹的终点(第 116 和 117 行)。这种随机化将在敌方区域的左右边界上选择两个坐标。此外,我们还随机化**transition(第 118 行)和duration**(第 119 行)以创建更多样化和不可预测的轨迹。

注意

Kivy 目前包含 31 种过渡。它们由一个字符串表示,例如'in_out_cubic',其中in_out是一个前缀,描述了函数(cubic)的使用方式。有三个可能的前缀(inoutin_out),以及 10 个函数(第 102 行),例如cubicexponentialsinquadratic。请访问 Kivy API 以了解它们的描述(kivy.org/docs/api-kivy.animation.html)。

第 118 行随机选择一个过渡。过渡应用于进度,因此同时应用于xy,这在轨迹上产生了一个有趣的加速度效果。

Animation类结束其轨迹(第 110 行)时,to_dock方法(第 121 至 126 行)将入侵者Window的顶部中心部分返回到其原始位置。我们使用**Window**类来获取heightwidth。有时这比遍历父级链以找到根小部件要简单。当入侵者到达停靠点时,它会被绑定回那里(第 125 行)。

最后一个方法(第 128 至 133 行的drop_missile)发射一枚从入侵者底部中心位置(第 130 行)开始垂直向下至屏幕底部的导弹(第 133 行)。记住,Missile类继承自我们在前一节中创建的Ammo类。

我们现在可以让入侵者自由地在敌方区域内移动。然而,我们还想有一种群体移动方式。在下一节中,我们将为每个相应的入侵者创建一个停靠点。这样,入侵者舰队队形中就有了一个对应的占位符。之后,我们将创建舰队,它将不断移动所有的停靠点

停靠点 – Kivy 语言中的自动绑定

你可能会从之前的章节中意识到,Kivy 语言不仅仅是将其规则转换为 Python 指令。例如,你可能会看到,当它创建属性时,它也会绑定它们。

注意

当我们在布局内部执行一些常见操作,例如 pos: self.parent.pos 时,父级的属性就会绑定到其子级。当父级移动时,子级总是移动到父级的位置。

这通常是期望的,但并非总是如此。考虑一下入侵者的 solo_attack。我们需要它打破队形,并在屏幕上遵循自由轨迹。当这种情况发生时,整个入侵者队形将继续从右向左和从左向右移动。这意味着入侵者将同时接收到两个命令;一个来自移动的父级,另一个来自轨迹的 Animation

这意味着我们需要为每个入侵者(invader)提供一个占位符(dock)。这样,当入侵者从单独攻击执行返回时,可以确保其空间。如果没有占位符,舰队(GridLayout,我们将在下一节中看到)的布局将自动重新配置队形,重新分配剩余的入侵者以填充空位。此外,入侵者还需要从父级(dock)中释放自己,以便可以在屏幕上的任何位置漂浮。以下代码(dock.py)使用 Python 而不是 Kivy 语言绑定(第 145 至 147 行)和解除绑定(第 149 至 151 行)入侵者:

134\. # File name: dock.py
135\. from kivy.uix.widget import Widget
136\. from invader import Invader
137\. 
138\. class Dock(Widget):
139\.   def __init__(self, **kwargs):
140\.     super(Dock, self).__init__(**kwargs)
141\.     self.invader = Invader()
142\.     self.add_widget(self.invader)
143\.     self.bind_invader()
144\. 
145\.   def bind_invader(self, instance=None, value=None):
146\.     self.invader.formation = True
147\.     self.bind(pos = self.on_pos)
148\. 
149\.   def unbind_invader(self):
150\.     self.invader.formation = False
151\.     self.unbind(pos = self.on_pos)
152\. 
153\.   def on_pos(self, instance, value):
154\.     self.invader.pos = self.pos

小贴士

我们使用 第三章 的知识,部件事件 – 绑定动作,来编写此代码,但重要的是我们应用的策略。

有时会希望避免使用 Kivy 语言,因为这更有利于完全控制。

这并不意味着使用 Kivy 语言解决这个问题是不可能的。例如,一种常见的方法是将入侵者的父级(dock)切换到,比如说,应用程序的根 Widget 实例;这将解除入侵者位置与其当前父级的绑定。我们遵循哪种方法并不重要。只要我们理解了机制,我们就能找到优雅的解决方案。

现在既然每个入侵者都有一个确保其在入侵者队形中位置的 dock,我们就准备好向舰队引入一些运动。

舰队 – 无限连接的动画

在本节中,我们将使舰队从右向左和从左向右进行动画处理,以保持持续运动,如以下截图中的箭头所示:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_05_05.jpg

为了做到这一点,我们将学习如何在动画完成之后立即连接另一个动画。实际上,我们将创建一个无限循环的动画,使得舰队处于持续运动状态。

小贴士

我们可以使用 on_complete 事件连接两个动画。

以下为 fleet.py 的代码片段 1(共 2 个),展示了如何连接这些事件:

155\. # File name: fleet.py (Fragment 1)
156\. from kivy.uix.gridlayout import GridLayout
157\. from kivy.properties import ListProperty
158\. from kivy.animation import Animation
159\. from kivy.clock import Clock
160\. from kivy.core.window import Window
161\. from random import randint, random
162\. from dock import Dock
163\. 
164\. class Fleet(GridLayout):
165\.   survivors = ListProperty(())
166\. 
167\.   def __init__(self,  **kwargs):
168\.     super(Fleet, self).__init__(**kwargs)
169\.     for x in range(0, 32):
170\.       dock = Dock()
171\.       self.add_widget(dock)
172\.       self.survivors.append(dock)
173\.     self.center_x= Window.width/4
174\. 
175\.   def start_attack(self, instance, value):
176\.     self.invasion.remove_widget(value)
177\.     self.go_left(instance, value)
178\.     self.schedule_events()
179\. 
180\.   def go_left(self, instance, value):
181\.     animation = Animation(x = 0)
182\.     animation.bind(on_complete = self.go_right)
183\.     animation.start(self)
184\. 
185\.   def go_right(self, instance, value):
186\.     animation = Animation(right=self.parent.width)
187\.     animation.bind(on_complete = self.go_left)
188\.     animation.start(self)

go_left 方法(第 180 至 183 行)将 Animation 实例的 on_complete(第 182 行)事件绑定到 go_right 方法(第 185 至 188 行)。同样,go_right 方法将另一个 Animation 实例的 on_complete(第 187 行)事件绑定到 go_left 方法。通过这种策略,我们创建了一个两个动画的无穷循环。

fleet.py 类还重载了构造函数,向 Fleet 的子类添加了 32 个 入侵者(第 169 至 173 行)。这些 入侵者 被添加到我们用来跟踪尚未被击落的 入侵者ListProperty 中。start_attack 方法(第 175 至 178 行)通过调用 go_left 方法(第 177 行)和 schedule_events 方法(第 178 行)启动 Fleet 动画。后者使用了 Clock,这将在下一节中解释。

使用时钟安排事件

我们看到 Animation 有一个持续时间参数,它确定了动画应该持续的时间。另一个与时间相关的话题是在特定时间或 n 秒的间隔内安排特定任务。在这些情况下,我们使用 Clock 类。让我们分析以下 fleet.py 的代码片段 2(共 2 个),如下所示:

189\. # File name: fleet.py (Fragment 2)
190\.   def schedule_events(self):
191\.     Clock.schedule_interval(self.solo_attack, 2)
192\.     Clock.schedule_once(self.shoot,random())
193\. 
194\.   def solo_attack(self, dt):
195\.     if len(self.survivors):
196\.       rint = randint(0, len(self.survivors) - 1)
197\.       child = self.survivors[rint]
198\.       child.invader.solo_attack()
199\. 
200\.   def shoot(self, dt):
201\.     if len(self.survivors):
202\.       rint = randint(0,len(self.survivors) - 1)
203\.       child = self.survivors[rint]
204\.       child.invader.drop_missile()
205\.       Clock.schedule_once(self.shoot,random())
206\. 
207\.   def collide_ammo(self, ammo):
208\.     for child in self.survivors:
209\.       if child.invader.collide_widget(ammo):
210\.         child.canvas.clear()
211\.         self.survivors.remove(child)
212\.         return True
213\.     return False
214\. 
215\.   def on_survivors(self, instance, value):
216\.     if len(self.survivors) == 0:
217\.       Clock.unschedule(self.solo_attack)
218\.       Clock.unschedule(self.shoot)
219\.       self.invasion.end_game("You Win!")

schedule_events 方法(第 190 至 192 行)为特定时间安排动作。第 191 行每两秒安排一次 solo_attack 方法。第 192 行随机安排一次 shoot(在 0 和 1 秒之间)。

注意

schedule_interval 方法定期安排动作,而 schedule_once 方法只安排一次动作。

solo_attack 方法(第 194 至 198 行)随机选择一个幸存者执行我们为 入侵者 研究的单独攻击(invader.py 中的第 106 至 111 行)。shoot 方法(第 200 至 205 行)随机选择一个幸存者向 射击者 发射 导弹(第 201 至 204 行)。之后,该方法安排另一个 shoot(第 205 行)。

Ammo 类中,我们使用了 collide_ammo 方法来验证 Ammo 实例是否击中了任何 入侵者ammo.py 中的第 83 行)。现在,在 fleet.py 中,我们实现了这样一个方法(第 207 或 213 行),它将 入侵者 隐藏并从幸存者列表中删除。每当修改幸存者 ListProperty 时,都会触发 on_survivors 事件。当没有幸存者剩下时,我们使用 unscheduled 方法(第 217 和 218 行)取消安排事件,并通过显示 You Win! 消息来结束游戏。

我们完成了射击者敌人的创建。现在,是时候为 射击者 提供躲避 导弹射击 以击中 入侵者 的移动了。

射击者 – 多点触控控制

Kivy 支持多点触控交互。这个特性始终存在,但我们除了在 第四章 改进用户体验 中使用 Scatter 小部件时之外,并没有过多关注它。我们没有明确指出整个屏幕和 GUI 组件已经能够进行多点触控,以及 Kivy 会相应地处理这些事件。

注意

Kivy 在内部处理多点触控动作。这意味着所有 Kivy 小部件和组件都支持多点触控交互;我们不必担心这一点。Kivy 解决了多点触控控制中常见的模糊情况的所有可能冲突,例如,同时触摸两个按钮。

话虽如此,控制特定实现的责任在我们身上。多点触控编程引入了我们需要作为开发者解决的逻辑问题。尽管如此,Kivy 提供了与每个特定触摸相关的数据,因此我们可以处理逻辑。主要问题是我们需要不断区分一个触摸与另一个触摸,然后采取相应的行动。

在《入侵者复仇》中,我们需要区分由相同类型的触摸触发的两种动作。第一种动作是 射手 的水平移动,以避免入侵者的 导弹。第二种是触摸屏幕以射击 入侵者。以下截图通过宽粗箭头(滑动触摸)和虚线细箭头(射击动作)说明了这两种动作:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_05_06.jpg

以下 shooter.py 的代码片段 1(共 2 个),通过使用 敌人区域射手区域 来控制这两种动作:

220\. # File name: shooter.py (Fragment 1)
221\. from kivy.clock import Clock
222\. from kivy.uix.image import Image
223\. from ammo import Shot
224\. 
225\. class Shooter(Image):
226\.   reloaded = True
227\.   alife = False
228\. 
229\.   def on_touch_down(self, touch):
230\.     if self.parent.collide_point(*touch.pos):
231\.       self.center_x = touch.x
232\.       touch.ud['move'] = True
233\.     elif self.enemy_area.collide_point(*touch.pos):
234\.       self.shoot(touch.x,touch.y)
235\.       touch.ud['shoot'] = True
236\. 
237\.   def on_touch_move(self, touch):
238\.     if self.parent.collide_point(*touch.pos):
239\.       self.center_x = touch.x
240\.     elif self.enemy_area.collide_point(*touch.pos):
241\.       self.shoot(touch.x,touch.y)
242\. 
243\.   def on_touch_up(self, touch):
244\.     if 'shoot' in touch.ud and touch.ud['shoot']:
245\.       self.reloaded = True

on_touch_down(第 229 至 235 行)和 on_touch_move(第 237 至 241 行)方法通过分别使用 射手区域(第 230 和 238 行)和 敌人区域(第 233 和 240 行)小部件来区分两种动作,移动射击,以便碰撞事件的坐标。

注意

触摸坐标是最常见的识别特定触摸的策略。然而,触摸有许多其他属性可以帮助区分它们,例如,时间、双击(或三击)或输入设备。您可以通过检查 MotionEvent 类来查看触摸的所有属性(kivy.org/docs/api-kivy.input.motionevent.html#kivy.input.motionevent.MotionEvent)。

与之相反,on_touch_up 方法(第 243 行)采用了一种不同的方法。它使用 MotionEvent 实例(触摸)的 ud 属性(用户数据字典,用于在触摸中存储个性化数据)来确定事件开始时的触摸是否是 移动(在 射手区域)还是 射击(在 敌人区域)。我们之前在 on_touch_down 中设置了 touch.ud(第 232 和 235 行)。

注意

Kivy 将触摸事件与三个基本触摸事件(按下、移动和抬起)关联起来,因此我们获取的on_touch_downon_touch_moveon_touch_up的触摸引用是相同的,我们可以区分触摸。

现在我们来分析这些事件调用的方法细节。以下是从shooter.py中提取的代码片段 2(共 2 个):

246\. # File name: shooter.py (Fragment 2) 
247\.   def start(self, instance, value):
248\.     self.alife=True
249\. 
250\.   def shoot(self, fx, fy):
251\.     if self.reloaded and self.alife:
252\.       self.reloaded = False
253\.       Clock.schedule_once(self.reload_gun, .5)
254\.       shot = Shot()
255\.       shot.center = (self.center_x, self.top)
256\.       self.invasion.add_widget(shot)
257\.       (fx,fy) = self.project(self.center_x,self.top,fx,fy)
258\.       shot.shoot(fx,fy,self.invasion.fleet)
259\. 
260\.   def reload_gun(self, dt):
261\.     self.reloaded = True
262\. 
263\.   def collide_ammo(self, ammo):
264\.     if self.collide_widget(ammo) and self.alife:
265\.       self.alife = False
266\.       self.color = (0,0,0,0)
267\.       self.invasion.end_game("Game Over")
268\.       return True
269\.     return False
270\. 
271\.   def project(self,ix,iy,fx,fy):
272\.     (w,h) = self.invasion.size
273\.     if ix == fx: return (ix, h)
274\.     m = (fy-iy) / (fx-ix)
275\.     b = iy - m*ix
276\.     x = (h-b)/m
277\.     if x < 0: return (0, b)
278\.     elif x > w: return (w, m*w+b)
279\.     return (x, h)

我们首先创建了一个方法来启动射手(第 247 和 248 行),我们将在游戏开始时使用它。然后,我们为on_touch_move方法与shoot方法(第 250 至 258 行)实现了一个有趣的行为。我们不是尽可能快地射击,而是将下一次shoot延迟0.5秒。这个延迟模拟了枪需要重新装填的时间间隔(第 253 行)。否则,如果允许计算机以尽可能快的速度射击,这对入侵者来说是不公平的。相反,当我们使用on_touch_up方法时,枪立即重新装填,因此在这种情况下,这将取决于玩家的技巧,看谁能够通过射门和触摸序列更快地射击。

collide_ammo方法(第 263 至 269 行)几乎等同于Fleet(第 207 至 213 行)中的collide_ammo方法。唯一的区别是只有一个射手而不是一组入侵者。如果射手被击中,则游戏结束,并显示游戏结束的消息。请注意,我们没有移除射手,我们只是将其alife标志设置为False(第 265 行),并通过将颜色设置为黑色(第 266 行)来隐藏它。这样,我们避免了指向不再存在于界面上下文中的实例的引用不一致。

project方法(第 271 至 278 行)将触摸坐标扩展到屏幕边界,因此射击将继续其轨迹直到屏幕的尽头,而不是正好停止在触摸坐标处。数学细节超出了本书的范围,但它是一种简单的线性投影。

应用程序几乎准备好了。只有一个小问题。如果你没有多点触控屏幕,实际上你将无法玩这个游戏。下一节将介绍如何处理键盘事件,以便采用更经典的游戏方式,这种方式结合了键盘和鼠标。

入侵 - 使用键盘移动射手

本节提供了第二种移动射手的方法。如果你没有多点触控设备,你将需要使用其他东西来轻松控制射手的位置,同时你使用鼠标进行射击。以下是从main.py中提取的代码片段 1(共 2 个):

280\. # File name: main.py (Fragment 1)
281\. from kivy.app import App
282\. from kivy.lang import Builder
283\. from kivy.core.window import Window
284\. from kivy.uix.floatlayout import FloatLayout
285\. from kivy.uix.label import Label
286\. from kivy.animation import Animation
287\. from kivy.clock import Clock
288\. from fleet import Fleet
289\. from shooter import Shooter
290\. 
291\. Builder.load_file('images.kv')
292\. 
293\. class Invasion(FloatLayout):
294\. 
295\.   def __init__(self, **kwargs):
296\.     super(Invasion, self).__init__(**kwargs)
297\.     self._keyboard = Window.request_keyboard(self.close,          self)
298\.     self._keyboard.bind(on_key_down=self.press)
399\.     self.start_game()
300\. 
301\.   def close(self):
302\.     self._keyboard.unbind(on_key_down=self.press)
303\.     self._keyboard = None
304\. 
305\.   def press(self, keyboard, keycode, text, modifiers):
306\.     if keycode[1] == 'left':
307\.       self.shooter.center_x -= 30
308\.     elif keycode[1] == 'right':
309\.       self.shooter.center_x += 30
310\.     return True
311\. 
312\.   def start_game(self):
313\.     label = Label(text='Ready!')
314\.     animation = Animation (font_size = 72, d=2)
315\.     animation.bind(on_complete=self.fleet.start_attack)
316\.     animation.bind(on_complete=self.shooter.start)
317\.     self.add_widget(label)
318\.     animation.start(label)

上一段代码展示了键盘事件控制。__init__构造函数(第 295 至 299 行)将请求**keyboard(第 297 行)到Window,并将on_keyboard_down事件绑定到press方法。Window._request_keyboard方法的一个重要参数是当keyboard关闭时调用的方法(第 301 至 303 行)。键盘可以关闭的原因有很多,包括当另一个小部件请求它时。press方法(第 305 至 310 行)负责处理键盘输入,即按下的键。按下的键保存在keycode参数中,并在第 306 和 308 行使用,以决定射手**应该向左还是向右移动。

注意

游戏中的键盘绑定是为了在没有多触控功能的设备上进行测试。如果您想在您的移动设备上尝试它,您应该注释掉第 297 和 298 行以禁用键盘绑定。

第 299 行调用了start_game方法(第 312 至 318 行)。该方法显示带有文本Ready!Label。请注意,我们在第 314 行将一个Animation实例应用于font_size。到目前为止,我们一直在使用动画通过xypos属性移动小部件。然而,动画可以与任何支持算术运算的属性一起工作(例如,String不支持此类运算;作为一个反例)。例如,我们可以使用它们来动画化Scatter的旋转或缩放。当动画完成后,它将同时启动舰队射手(第 315 和 316 行)。注意我们如何将两个方法绑定到同一个事件。

小贴士

我们可以将任意数量的方法绑定到事件上。

在下一节中,我们将讨论如何按顺序或同时动画化多个属性。

使用+&结合动画

您已经了解到,您可以将多个属性添加到同一个动画中,以便它们一起修改(在ammo.py的第 69 行)。

注意

我们可以通过使用+&运算符来组合动画。+运算符用于创建顺序动画(一个接一个)。&运算符允许我们同时执行两个动画。

以下代码是main.py的片段 2,并展示了这两个运算符的使用:

319\. # File name: main.py (Fragment 2)
320\.   def end_game(self, message):
321\.     label = Label(markup=True, size_hint = (.2, .1), 
322\.       pos=(0,self.parent.height/2), text = message)
323\.     self.add_widget(label)
324\.     self.composed_animation().start(label)
325\. 
326\.   def composed_animation(self):
327\.     animation = Animation (center=self.parent.center)
328\.     animation &= Animation (font_size = 72, d=3)
329\.     animation += Animation(font_size = 24,y=0,d=2)
330\.     return animation
331\. 
332\. class InvasionApp(App):
333\.   def build(self):
334\.     return Invasion()
335\. 
336\. if __name__=="__main__":
337\.   InvasionApp().run()

end_game方法(第 320 至 324 行)显示一条最终消息,以指示游戏如何结束(在fleet.py的第 219 行显示You Win或在shooter.py的第 267 行显示Game Over)。该方法使用composed_animation方法(第 326 至 330 行)创建一个组合的Animation,在其中我们使用所有可能的组合动画方式。第 327 行是一个简单的Animation,它通过&运算符与另一个不同持续时间的简单Animation同时执行。在第 329 行,一个包含两个属性(font_sizey)的Animation通过+运算符附加到之前的一个上。

生成的动画执行以下操作:将消息从左侧移动到中间需要一秒钟,同时字体大小在增加。当它到达中间时,字体大小的增加持续两秒钟。一旦字体达到完全大小(72 点),消息移动到底部,同时字体大小以相同的速度减小。以下图表说明了整个动画序列:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_05_08.jpg

'+'运算符类似于我们在将Animation on_complete事件绑定到创建另一个Animation的方法时所做的操作:animation.bind(on_complete = self.to_dock)invader.py的第 110 行)。区别在于,当我们使用'+'运算符时,没有机会创建无限循环,就像我们在*fleet*中做的那样,或者在进行另一个动画之前更改Widget属性。例如,在*invader*的情况下,我们在将其带回到*dock*的动画之前(第 124 到 126 行),将*invader*移动到屏幕的顶部中心(第 122 和 123 行):

121\.   def to_dock(self, instance, value):
122\.     self.y = Window.height
123\.     self.center_x = Window.width/2
124\.     animation = Animation(pos=self.parent.pos, d=2)
125\.     animation.bind(on_complete = self.parent.bind_invader)
126\.     animation.start(self)

&运算符类似于将两个属性作为Animation的参数发送,就像我们在第 69 行所做的那样:self.animation = Animation(x=tx, top=ty)。将两个属性作为参数发送的区别在于,它们共享相同的持续时间和过渡效果,而在第 328 行,我们改变了第二个属性的持续时间。

这里有一张最后的截图,展示了入侵者最终复仇的场景:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_05_07.jpg

概述

本章涵盖了整个交互式和动画应用程序的构建过程。你学习了如何集成各种 Kivy 组件,现在你应该能够轻松地构建一个 2D 动画游戏。

让我们回顾一下本章中使用的所有新类和组件:

  • Atlas

  • Image: source属性

  • SoundLoaderSound: 分别是loadplay方法

  • Window: 高度宽度属性,以及request_keyboardremove_widgetadd_widget方法

  • Animation: 作为参数的属性;dt参数;startstopbind方法;on_starton_progresson_complete事件;以及'+''&'运算符

  • Touch: ud属性

  • Clock: schedule_intervalschedule_once方法

  • Keyboard: bindunbind方法,on_key_down事件

本章包含的信息提供了你可以用来开发高度交互式应用程序的工具和策略。通过将前几章的信息与本章对属性、绑定事件和 Kivy 语言进一步理解的洞察相结合,你应该能够快速开始使用 Kivy API 的所有其他组件(kivy.org/docs/api-kivy.html)。

最后一章,第六章, Kivy Player – TED 视频流媒体,本书将教你如何控制多媒体组件,特别是视频和音频。它将提供一个额外的示例,以便展示更多 Kivy 组件,但更重要的是,它将教你如何构建一个看起来更专业的界面。它还将介绍一些 Kivy 工具来调试我们的应用程序。

第六章. Kivy 播放器 – TED 视频流器

在本章中,我们将学习如何搜索、显示和控制视频。我们将整合前几章的知识,构建一个能够适应不同屏幕并最大化空间使用的响应式应用程序。我们将制作一个带有控件和字幕支持的增强型视频小部件,并学习如何显示来自 TED API 服务的搜索结果查询(developer.ted.com)。以下是本章我们将涵盖的主要主题:

  • 控制流媒体视频的播放进度

  • 使用视频的进度在正确的时间显示字幕

  • 应用策略和组件使我们的应用程序响应

  • 显示和导航本地文件目录树

  • 使用 Kivy 检查器调试我们的应用程序

  • 向从互联网查询得到的列表结果添加滚动功能

本章总结了迄今为止获得的大量知识。我们将回顾和结合使用属性、事件、动画、触摸、行为、布局,甚至图形。同时,我们将介绍新的小部件,这些小部件将补充您的知识,并作为新编程情况的好例子。我们还将回顾 Kivy 检查器,它将帮助我们检测 GUI 错误。在本章结束时,我们将完成一个看起来专业的界面。

视频 – 播放、暂停和停止

在本节中,我们将从简单的代码开始,然后逐步添加功能,直到我们得到一个完整的视频播放器。在本节中,我们将讨论如何使用**Video小部件从互联网上流式传输视频**。让我们从video.kv文件中的代码开始:

1\. # File name: video.kv
2\. #:set _default_video "http://video.ted.com/talk/stream/2004/None/DanGilbert_2004-180k.mp4"
3\. 
4\. <Video>:
5\.     allow_stretch: True
6\.     color: 0,0,0,0
7\.     source: _default_video

在此代码中,我们最初使用**set**指令创建一个常量值(第 2 行)。此指令允许我们在 Kivy 语言范围内使用全局值。例如,我们使用_default_video常量的值设置Video类的source属性(第 7 行)。

我们为**Video类设置了三个属性。allow_stretch属性(第 5 行)允许视频根据可用的屏幕大小进行拉伸。color属性(第 6 行)将使视频变黑,当视频未播放时用作前景(以及封面图像的背景)。source属性(第 7 行)包含我们想要播放的视频的 URL(或文件名)。这三个属性实际上属于Image**小部件,它是Video的基础类。如果我们把视频看作是一系列图像(伴随声音),这就有意义了。

小贴士

为了测试目的,如果您想避免不断从互联网下载视频(或者如果 URL 已经不可用),您可以将default_video中的 URL 替换为与代码一起提供的示例文件:samples/BigBuckBunny.ogg

我们将使用 Factory 类来使用我们在 第四章 中学到的技术,改进用户体验。当时,我们使用 Factory 类来替换 Line 顶点指令,使用我们的个性化实现,一个滚动的 Line

注意

Factory 类遵循一种面向对象的软件设计模式,称为工厂模式。工厂模式根据调用标识符(通常是方法)返回默认的新对象(实例)的子集类。在 Kivy 语言的情况下,我们只使用一个名称。(en.wikipedia.org/wiki/Factory_%28object-oriented_programming%29)。

我们现在将做类似的事情,但这次我们将个性化我们的 Video 小部件:

8\. # File name: video.py
9\. from kivy.uix.video import Video as KivyVideo
10\. 
11\. from kivy.factory import Factory
12\. from kivy.lang import Builder
13\. 
14\. Builder.load_file('video.kv')
15\. 
16\. class Video(KivyVideo): 
17\. 
18\.     def on_state(self, instance, value):
19\.         if self.state == 'stop':
20\.             self.seek(0) 
21\.         return super(self.__class__, self).on_state(instance, value)
22\. 
23\.     def on_eos(self, instance, value):
24\.         if value:
25\.             self.state = 'stop'
26\. 
27\.     def _on_load(self, *largs):
28\.         super(self.__class__, self)._on_load(largs)
29\.         self.color = (1,1,1,1)
30\. 
31\.     def on_source(self, instance, value):
32\.         self.color = (0, 0, 0, 0)
33\. 
34\. Factory.unregister('Video')
35\. Factory.register('Video', cls=Video)

video.py 文件将导入 Kivy 的 Video 小部件,并使用别名 KivyVideo(第 9 行)。现在我们将能够使用 Video 类名称(而不是不那么吸引人的替代名称,如 MyVideo)创建我们的个性化小部件(第 16 行至 32 行)。在文件末尾,我们将默认的 Video 小部件替换为我们的个性化 Video 并将其添加到 Factory 中(第 34 行和 35 行)。从现在起,Kivy 语言中引用的 Video 类将对应于我们在此文件中的实现。

我们在 Video 类中创建了四个方法(on_stateon_eos_on_loadon_source)。所有这些都对应于事件:

  • 当视频在其三种可能状态(播放 'play')、暂停 ('pause') 或停止 ('stop') 之间改变状态时,会调用 on_state 方法(第 18 行)。我们确保当视频停止时,使用 seek 方法(第 20 行)将其重新定位到开始位置。

  • 当达到 流结束EOS)时,将调用 on_eos 方法(第 23 行)。当发生这种情况时,我们将确保将状态设置为 stop(第 19 行)。

  • 我们还需要记住,我们使用 Kivy 语言的 color 属性(第 6 行)将视频染成了黑色。因此,我们需要在视频上放置亮光(1,1,1,1)才能看到它(第 29 行)。当视频被加载到内存中并准备好播放时,会调用 _on_load 方法(第 27 行)。我们使用此方法来设置适当的(以及原始 Kivy 默认的)color 属性。

    注意

    记住 第二章,图形 – 画布 中,Image 小部件(Video 类的基类)的 color 属性在显示上充当着染色或光照。对于 Video 小部件,也会发生同样的效果。

  • 最后,从 Image 类继承的 on_source 方法将在更改视频源时在视频上方恢复黑色染色。

让我们继续创建一个 kivyplayer.py 文件来执行我们的应用程序,并播放、暂停和停止我们的视频:

36\. # File name: kivyplayer.py
37\. from kivy.app import App
38\. 
39\. from video import Video
40\. 
41\. class KivyPlayerApp(App):
42\. 
43\.     def build(self):
44\.         self.video = Video()
45\.         self.video.bind(on_touch_down=self.touch_down)
46\.         return self.video
47\. 
48\.     def touch_down(self, instance, touch):
49\.         if self.video.state == 'play':
50\.             self.video.state = 'pause'
51\.         else:
52\.             self.video.state = 'play'
53\.         if touch.is_double_tap:
54\.             self.video.state = 'stop'
55\. 
56\. if __name__ == "__main__":
57\.     KivyPlayerApp().run()

目前,我们将通过触摸来控制视频。在build方法(第 43 行)中,我们将视频的on_touch_down事件(第 45 行)绑定到touch_down方法(第 48 至 54 行)。一次触摸将根据视频当前的**state属性(第 49 和 52 行)播放或暂停视频。状态属性控制视频是否处于三种可能状态之一。如果它在播放,则将其暂停;否则(暂停或停止),则播放它。我们将使用double_tap**键来表示双击(双击或双击),以停止视频。下次我们触摸屏幕时,视频将从开头开始。现在,运行应用程序(Python kivyplayer.py),看看当你点击屏幕时,Kivy 是如何立即开始从 TED 流式传输 Dan Gilbert 的视频《幸福的惊人科学》的(www.ted.com/):

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_06_01.jpg

AsyncImage – 为视频创建封面

在本节中,我们将学习如何设置一个将在视频不播放时显示的封面。当视频尚未开始时,此图像将作为装饰,在 TED 视频中,通常涉及演讲者的图像。让我们从在video.kv代码中引入一些更改开始:

58\. # File name: video.kv 
59\. ...
60\. #:set _default_image "http://images.ted.com/images/ted/016a827cc0757092a0439ab2a63feca8655b6c29_1600x1200.jpg"
61\. 
62\. <Video>:
63\.     cover: _cover
64\.     image: _default_image
65\.     ...
66\.     AsyncImage:
67\.         id: _cover
68\.         source: root.image
69\.         size: root.width,root.height

在此代码中,我们使用**set**指令(第 60 行)创建了一个另一个常量(_default_image),并为Video类创建了一个相关属性(image),该属性引用了常量(第 64 行)。我们还创建了cover属性(第 63 行),以引用我们添加到Video类中的AsyncImage(第 66 行),它将作为视频的封面。

注意

ImageAsyncImage之间的主要区别在于,**AsyncImage**小部件允许我们在图片加载时继续使用程序,而不是在图片完全下载之前阻塞应用程序。

这很重要,因为我们从互联网上下载图像,它可能是一个大文件。当你运行代码时,你会注意到在图像加载时会出现一个等待的图像:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_06_02.jpg

我们还设置了一些AsyncImage属性。我们使用新属性(root.image)(第 68 行)初始化了source属性,该属性我们在Video小部件中创建,以引用封面图像(第 64 行)。请记住,这将内部绑定属性,这意味着每次我们更改image属性时,source属性都将更新为相同的值。第 69 行重复了相同的思想,以保持封面的size属性与视频的尺寸相等。

小贴士

为了测试目的,您可以将default_image中的 URL 替换为代码中包含的以下示例文件:

samples/BigBuckBunny.png

我们将对我们的视频小部件进行一些修改,以确保在播放视频时封面被移除(隐藏):

70\. # File name: video.py
71\.  ...
72\. from kivy.properties import ObjectProperty
73\.  ...
74\. class Video(KivyVideo): 
75\.     image = ObjectProperty(None)
76\. 
77\.     def on_state(self, instance, value):
78\.         if self.state == 'play':
79\.             self.cover.opacity = 0
80\.         elif self.state == 'stop':
81\.             self.seek(0) 
82\.             self.cover.opacity = 1
83\.         return super(self.__class__, self).on_state(instance, value)
84\. 
85\.     def on_image(self, instance, value):
86\.         self.cover.opacity = 1
87\.  ...

我们将 on_state 方法改为在视频播放时(第 79 行)揭示视频,并在视频停止时(第 82 行)再次使用 不透明度 属性覆盖它。

提示

避免移除在 .kv 文件中声明的部件。大多数情况下,这些部件与其他部件(例如,属性边界)有内部边界,并可能导致与缺失内部引用和不一致的边界属性相关的意外运行时错误。

与移除部件相比,有几种替代方案;例如,首先,使用 opacity 属性使部件不可见,其次,使用 size 属性将部件区域设置为零(size = (0,0)),最后,使用 pos 属性将部件放置在一个永远不会显示的位置(pos= (99999,999999))。我们选择了第一种方法;在这种情况下,这是最优雅的。我们将 AsyncImageopacity 属性设置为使其可见(opacity = 1)或不可见(opacity = 0)。

提示

尽管使用不透明度控制覆盖以使其不可见可能是这里最优雅的解决方案,但你必须小心,因为部件仍然存在,占据了屏幕上的空间。根据情况,你可能需要扩展策略。例如,如果部件捕获了一些触摸事件,你可以结合 opacitydisabled 属性来隐藏和禁用部件。

我们还创建了 image 属性(第 75 行),并使用其 on_image 关联事件(第 85 行)确保在更改图像时恢复不透明度(第 86 行)。现在,当你运行应用程序时(python kivyplayer.py),将出现丹·吉尔伯特的图像。

字幕 – 跟踪视频进度

让我们在我们的应用程序中添加字幕。我们将通过四个简单的步骤来完成这项工作:

  1. 创建一个从 Label 类派生的 Subtitle 部件(subtitle.kv),用于显示字幕

  2. 在视频部件上方放置一个 Subtitle 实例(video.kv

  3. 创建一个 Subtitles 类(subtitles.py),用于读取和解析字幕文件

  4. 跟踪 Video 进度(video.py)以显示相应的字幕

步骤 1 包括在 subtitle.kv 文件中创建一个新的部件:

88\. # File name: subtitle.kv
89\. <Subtitle@Label>:
90\.     halign: 'center'
91\.     font_size: '20px'
92\.     size: self.texture_size[0] + 20, self.texture_size[1] + 20
93\.     y: 50
94\.     bcolor: .1, .1, .1, 0
95\.     canvas.before:
96\.         Color:
97\.             rgba: self.bcolor
98\.         Rectangle:
99\.             pos: self.pos
100\.             size: self.size

这段代码中有两个有趣的元素。第一个是大小属性的定义(第 92 行)。我们将其定义为比 texture_size 宽度和高度大 20 像素。texture_size 属性表示由字体大小和文本确定的文本大小,我们使用它来调整 Subtitles 部件的大小以适应其内容。

注意

texture_size 是一个只读属性,因为它的值是根据其他参数计算的,例如字体大小和文本显示的高度。这意味着我们将从这个属性中读取,但不会在其上写入。

第二个元素是创建bcolor属性(第 94 行)以存储背景颜色,以及如何将矩形的rgba颜色绑定到它(第 97 行)。Label小部件(像许多其他小部件一样)没有背景颜色,创建一个矩形是创建此类功能的常用方法。我们添加bcolor属性是为了从实例外部更改矩形的颜色。

提示

我们不能直接修改顶点指令的参数;然而,我们可以创建控制顶点指令内部参数的属性。

让我们继续之前提到的第 2 步。我们需要在video.kv文件中向当前的Video小部件添加一个Subtitle实例:

101\. # File name: video.kv 
102\. ...
103\. #:set _default_surl "http://www.ted.com/talks/subtitles/id/97/lang/en"
104\. 
105\. <Video>:
106\.     surl: _default_surl
107\.     slabel: _slabel
108\.     ...
109\. 
110\.     Subtitle:
111\.         id: _slabel
112\.         x: (root.width - self.width)/2

我们添加了一个名为_default_surl的另一个常量变量(第 103 行),其中包含对应 TED 视频字幕文件的 URL 链接。我们将此值设置为surl属性(第 106 行),这是我们刚刚创建的用于存储字幕 URL 的属性。我们添加了slabel属性(第 107 行),通过其 ID 引用Subtitle实例(第 111 行)。然后我们确保字幕居中(第 112 行)。

为了开始第 3 步(解析字幕文件),我们需要查看 TED 字幕的格式:

113\. {
114\.     "captions": [{
115\.         "duration":1976,
116\.         "content": "When you have 21 minutes to speak,",
117\.         "startOfParagraph":true,
118\.         "startTime":0,
119\.     }, ...

TED 使用一个非常简单的 JSON 格式(en.wikipedia.org/wiki/JSON),其中包含一个字幕列表。每个caption包含四个键,但我们只会使用durationcontentstartTime。我们需要解析这个文件,幸运的是,Kivy 提供了一个**UrlRequest**类(第 121 行),它将为我们完成大部分工作。以下是创建Subtitles类的subtitles.py代码:

120\. # File name: subtitles.py
121\. from kivy.network.urlrequest import UrlRequest
122\. 
123\. class Subtitles:
124\. 
125\.     def __init__(self, url):
126\.         self.subtitles = []
127\.         req = UrlRequest(url, self.got_subtitles)
128\. 
129\.     def got_subtitles(self, req, results):
130         self.subtitles = results['captions']
131\. 
132\.     def next(self, secs):
133\.         for sub in self.subtitles:
134\.             ms = secs*1000 - 12000
135\.             st = 'startTime'
136\.             d = 'duration'
137\.             if ms >= sub[st] and ms <= sub[st] + sub[d]:
138\.                 return sub
139\.         return None 

Subtitles类的构造函数将接收一个 URL(第 125 行)作为参数。然后,它将发出请求以实例化**UrlRequest类(第 127 行)。类实例化的第一个参数是请求数据的 URL,第二个参数是当请求数据返回(下载)时调用的方法。一旦请求返回结果,就会调用got_subtitles方法(第 129 行)。UrlRequest**提取 JSON 并将其放置在got_subtitles的第二个参数中。我们只需将字幕放入一个类属性中,我们称之为subtitles(第 130 行)。

next方法(第 132 行)接收秒数(secs)作为参数,并将遍历加载的 JSON 字典以查找对应于该时间的字幕。一旦找到,该方法就返回它。我们减去了12000微秒(第 134 行,ms = secs*1000 - 12000),因为 TED 视频在演讲开始前大约有 12 秒的介绍。

一切都为第 4 步做好了准备,在这一步中,我们将所有部件组合起来以查看字幕是否工作。以下是video.py文件头部的修改:

140\. # File name: video.py
141\. ...
142\. from kivy.properties import StringProperty
143\. ...
144\. from kivy.lang import Builder
145\. 
146\. Builder.load_file('subtitle.kv')
147\. 
148\. class Video(KivyVideo):
149\.     image = ObjectProperty(None)
150\.     surl = StringProperty(None)

我们导入了StringProperty并添加了相应的属性(第 142 行)。在本章结束时,我们将使用此属性在 GUI 中切换 TED 演讲。目前,我们只需使用在video.kv中定义的_default_surl(第 150 行)。我们还加载了subtitle.kv文件(第 146 行)。现在,让我们分析对video.py文件的其他更改:

151\.     ...
152\.     def on_source(self, instance, value):
153\.         self.color = (0,0,0,0)
154\.         self.subs = Subtitles(name, self.surl)
155\.         self.sub = None
156\. 
157\.     def on_position(self, instance, value):
158\.         next = self.subs.next(value)
159\.         if next is None:
160\.             self.clear_subtitle()
161\.         else:
162\.             sub = self.sub
163\.             st = 'startTime'
164\.             if sub is None or sub[st] != next[st]:
165\.                 self.display_subtitle(next)
166\. 
167\.     def clear_subtitle(self):
168\.         if self.slabel.text != "":
169\.             self.sub = None
170\.             self.slabel.text = ""
171\.             self.slabel.bcolor = (0.1, 0.1, 0.1, 0)
172\. 
173\.     def display_subtitle(self, sub):
174\.         self.sub = sub
175\.         self.slabel.text = sub['content']
176\.         self.slabel.bcolor = (0.1, 0.1, 0.1, .8)
177\. (...)

我们在on_source方法中添加了一些代码行,以便使用surl属性初始化字幕属性为Subtitles实例(第 154 行),并初始化包含当前显示字幕的sub属性(如果有的话)(第 155 行)。

现在,让我们研究如何跟踪进度以显示相应的字幕。当视频在Video小部件内播放时,每秒都会触发on_position事件。因此,我们在on_position方法(第 157 至 165 行)中实现了显示字幕的逻辑。每次调用on_position方法(每秒),我们都会要求Subtitles实例(第 158 行)提供下一个字幕。如果没有返回任何内容,我们使用clear_subtitle方法(第 160 行)清除字幕。如果当前秒已经有一个字幕(第 161 行),那么我们确保没有字幕正在显示,或者返回的字幕不是我们已显示的字幕(第 164 行)。如果条件满足,我们使用display_subtitle方法(第 165 行)显示字幕。

注意到clear_subtitle(第 167 至 171 行)和display_subtitle(第 173 至 176 行)方法使用bcolor属性来隐藏字幕。这是在不从其父元素中删除的情况下使小部件不可见的另一个技巧。让我们看看以下屏幕截图中我们的视频和字幕的当前结果:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_06_03.jpg

控制栏 – 添加按钮以控制视频

在本节中,我们将处理用户与应用程序的交互。目前,我们通过在屏幕上触摸来控制视频,实现播放、暂停和停止视频。然而,这对我们应用程序的新用户来说并不直观。因此,让我们添加一些按钮来提高我们应用程序的可用性。

我们将使用带有ToggleButtonBehaviourToggleBehaviour类的Image小部件来创建播放/暂停按钮和停止按钮。以下是本节中将实现的简单控制栏的截屏:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_06_04.jpg

让我们从定义我们的两个controlbar.kv小部件开始。我们将逐一介绍每个小部件。让我们从文件的标题和ControlBar类定义开始:

178\. # File name: controlbar.kv
179\. <ControlBar@GridLayout>:
180\.     rows: 1
181\.     size_hint: None, None
182\.     pos_hint: {'right': 1}
183\.     padding: [10,0,0,0]
184\.     play_pause: _play_pause
185\.     progress: 0

我们从GridLayout类派生了ControlBar类,并设置了一些熟悉的属性。我们还创建了对播放/暂停按钮的引用,以及一个新属性(progress),它将跟踪视频的进度百分比(从 0 到 1)。让我们继续处理第一个嵌入的小部件,VideoPlayPause

186\.     VideoPlayPause:
187\.         id: _play_pause
188\.         start: 'atlas://data/images/defaulttheme/media-playback-start'
189\.         pause: 'atlas://data/images/defaulttheme/media-playback-pause'
190\.         size_hint: [None, None]
191\.         width: 44
192\.         source: self.start if self.state == 'normal' else self.pause

正如我们在controlbar.py中将要看到的,VideoPlayPauseImageToggleButtonBehavior的组合。我们以这种方式实现了source属性(第 192 行),即根据state属性的变化(normaldown)改变小部件的图像。现在让我们看看VideoStop的代码:

193\.     VideoStop:
194\.         size_hint: [None, None]
195\.         width: 44
196\.         source: 'atlas://data/images/defaulttheme/media-playback-stop'
197\.         on_press: self.stop(root.parent.video, _play_pause)

除了定义一些熟悉的属性外,我们还把事件on_press绑定到了stop方法(第 197 行),这将在相应的controlbar.py文件中展示。请注意,我们假设根的父元素包含对视频的引用(root.parent.video)。我们将在controlbar.py中继续这个假设:

198\. # File name: controlbar.py 
199\. from kivy.uix.behaviors import ButtonBehavior, ToggleButtonBehavior
200\. from kivy.uix.image import Image
201\. from kivy.lang import Builder
202\. 
203\. Builder.load_file('controlbar.kv')
204\. 
205\. class VideoPlayPause(ToggleButtonBehavior, Image):
206\.     pass
207\. 
208\. class VideoStop(ButtonBehavior, Image):
209\. 
210\.     def stop(self, video, play_pause):
211\.         play_pause.state = 'normal'
212\.         video.state = 'stop'

此代码导入了必要的类以及'controlbar.kv'(第 198 到 203 行)。然后,使用多重继承,它将VideoPlayPauseVideoStop类定义为Image类和适当行为的组合(第 205 和 208 行)。VideoStop类包含一个stop方法,当按钮被按下时调用(第 208 行)。这将把播放/暂停按钮状态设置为正常并停止视频(第 212 行)。

我们还将在videocontroller.kv文件中定义一个视频控制器,它将是控制栏视频的父元素:

213\. # File name: videocontroller.kv
214\. <VideoController >:
215\.     video: _video
216\.     control_bar: _control_bar
217\.     play_pause: _control_bar.play_pause
218\.     control_bar_width: self.width
219\.     playing: _video.state == 'play'
220\. 
221\.     Video:
222\.         id: _video
223\.         state: 'pause' if _control_bar.play_pause.state == 'normal' else 'play'
224\. 
225\.     ControlBar:
226\.         id: _control_bar
227\.         width: root.control_bar_width
228\.         progress: _video.position / _video.duration

首先,我们为VideoContoller定义了五个属性(第 215 到 219 行):videocontrol_barplay_pausecontrol_bar_widthplaying。前三个属性引用界面组件,control_bar_width将用于外部控制控制栏的宽度,而playing属性将指示视频是否正在播放(第 219 行)。

然后,我们添加了一个Video实例(第 221 行),其状态将取决于播放/暂停按钮的状态(第 223 行),以及一个ControlBar实例。控制栏width属性将由我们之前创建的control_bar_width(第 227 行)控制,而progress属性将以持续时间的百分比表示(第 228 行)。

现在,我们需要在各自的videocontroller.py文件中创建VideoController类:

229\. # File name: videocontroller.py
230\. from kivy.uix.floatlayout import FloatLayout
231\. from kivy.lang import Builder
232\. 
233\. import video
234\. import controlbar
235\. 
236\. Builder.load_file('videocontroller.kv')
237\. 
238\. class VideoController(FloatLayout):
239\.     pass

我们只包含了必要的导入,并将VideoController定义为FloatLayout的派生类。kivyplayer.py文件也必须更新,以便显示VideoController实例而不是Video

240\. # File name: kivyplayer.py
241\. from kivy.app import App
242\. from videocontroller import VideoController
243\. 
244\. class KivyPlayerApp(App):
245\.     def build(self):
246\.         return VideoController()
247\. 
248\. if __name__=="__main__":
249\.     KivyPlayerApp().run()

随意再次运行应用程序以测试播放/暂停停止按钮。下一节将向我们的应用程序引入进度条

滑块 - 包括进度条

在本节中,我们将介绍一个新的小部件,称为 Slider。这个小部件将作为 进度条,同时允许用户快进和倒退视频。我们将 进度条 集成到 控制栏 中,如下面的裁剪截图所示:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_06_05.jpg

如您所见,Slider 出现在 播放/暂停停止 按钮的左侧。让我们将 controlbar.kv 修改为添加 Slider 以反映这种顺序。让我们从文件的标题和 ControlBar 类的定义开始:

250\. # File name: controlbar.kv
251\. <ControlBar@GridLayout>:
252\.     ...
253\.     VideoSlider:
254\.         value: root.progress
255\.         max: 1
256\.     VideoPlayPause:
257\.         ...

VideoSlider 将使用视频的进度来更新其 value 属性。value 属性表示滑块在条上的位置,而 max 属性是它可以取的最大值。在这种情况下,1 是合适的,因为我们用从 0 到 1 的百分比(表示持续时间)来表示进度(第 255 行)。

现在我们将 VideoSlider 的定义添加到 controlbar.py 文件中:

258\. # File name: controlbar.py 
259\. ...
260\. class VideoSlider(Slider):
261\. 
262\.     def on_touch_down(self, touch):
263\.         video = self.parent.parent.video
264\.         if self.collide_point(*touch.pos):
265\.             self.prev_state = video.state
266\.             self.prev_touch = touch
267\.             video.state = 'pause'
268\.         return super(self.__class__, self).on_touch_down(touch)
269\. 
270\.     def on_touch_up(self, touch):
271\.         if self.collide_point(*touch.pos) and \
272\.             hasattr(self, 'prev_touch') and \
273\.             touch is self.prev_touch:
274\.             video = self.parent.parent.video
275\.             video.seek(self.value)
276\.             if prev_state != 'stop':
277\.                 video.state = self.prev_state
278\.         return super(self.__class__, self).on_touch_up(touch)

使用滑块控制视频的进度很棘手,因为视频和滑块需要不断互相更新。视频通过更新滑块来指示其进度,而当用户想要快进或倒退视频时,滑块会更新视频。这创建了一个复杂的逻辑,我们必须考虑以下因素:

  1. 我们需要使用触摸事件,因为我们想确保是用户在移动滑块,而不是视频进度。

  2. 似乎存在一个无限循环;我们更新了滑块,滑块上传了视频,然后视频又更新了滑块。

  3. 用户可能不仅会点击滑块,还可能拖动它,在拖动过程中,视频会再次更新滑块。

由于这些原因,我们需要执行以下步骤:

  1. 在更新进度之前暂停视频(第 267 行)。

  2. 不要直接使用值属性更新滑块,而是使用 seek 方法(第 275 行)更新视频进度。

  3. 使用两个事件 on_touch_down(第 262 行)和 on_touch_up(第 270 行),以便安全地更改视频的进度百分比。

on_touch_down方法(第 262 至 268 行)中,我们存储了视频的当前状态(第 265 行),以及触摸的引用(第 266 行),然后我们暂停了视频(第 267 行)。如果我们不暂停视频,在更新视频到滑块的进度之前,视频的进度可能会影响滑块(记住滑块的value在第 254 行绑定到progression属性)。在on_touch_up事件中,我们确保触摸实例与我们在on_touch_down方法中存储的实例相对应(第 272 和 273 行)。然后,我们使用**seek**方法(第 275 行)根据滑块的位置设置视频到正确的位置。最后,如果视频的状态与stop不同,我们重新建立视频的先前状态(第 276 和 277 行)。

随意再次运行应用程序。您还可以通过不同的选项和滑块来更新视频进行实验。例如,尝试在拖动滑块通过on_touch_move事件时实时更新。

动画 – 隐藏小部件

在本节中,我们将使控制栏在视频开始播放时消失,以便在没有视觉干扰的情况下观看视频。我们需要更改videocontroller.py文件以动画化ControlBar实例:

279\. # File name: videocontroller.py
280\. from kivy.animation import Animation
281\. from kivy.properties import ObjectProperty
282\. ...
283\. class VideoController(FloatLayout):
284\.     playing = ObjectProperty(None)
285\. 
286\.     def on_playing(self, instance, value):
287\.         if value:
288\.             self.animationVB = Animation(top=0)
289\.             self.control_bar.disabled = True
290\.             self.animationVB.start(self.control_bar)
291\.         else:
292\.             self.play_pause.state = 'normal'
293\.             self.control_bar.disabled = False
294\.             self.control_bar.y = 0
295\. 
296\.     def on_touch_down(self, touch):
297\.         if self.collide_point(*touch.pos):
298\.             if hasattr(self, 'animationVB'):
299\.                self.animationVB.cancel(self.control_bar)
300\.             self.play_pause.state = 'normal'
301\.         return super(self.__class__, self).on_touch_down(touch)

除了文件开头必要的导入(第 280 和 281 行)之外,我们还引入了playing属性(第 284 行)以及与on_playing事件和on_touch_down事件相关的两个方法。playing属性已经在 Kivy 语言中定义(第 219 行),但请记住,由于文件解析顺序,如果我们想在同一类中使用该属性,我们还需要在 Python 语言中定义它。

playing属性改变时,会触发on_playing事件(第 286 行)。此方法开始一个动画(第 290 行)并在视频播放时禁用控制栏(第 289 行)。动画将隐藏屏幕底部的控制栏。当视频不播放时,on_playing方法也会恢复控制栏(第 292 至 294 行),使其再次可见。

由于控制栏将在视频播放时隐藏,我们需要一种替代方法来停止视频(不同于停止按钮)。这就是为什么我们包括了on_touch_down事件(第 296 行)。一旦我们触摸屏幕,如果存在动画,动画将被取消(第 298 行),并将播放/暂停按钮设置为'normal'(第 300 行)。这将暂停视频并因此触发我们刚刚定义的on_playing事件(在这种情况下,因为它停止了播放)。

您现在可以再次运行应用程序并欣赏当我们按下播放/暂停按钮时,控制栏如何缓慢地消失到屏幕底部。

Kivy 检查器 – 调试界面

有时,我们在实现我们的界面时会遇到问题,特别是当许多部件没有图形显示时,理解出了什么问题可能很困难。在本节中,我们将使用本章中创建的应用程序来介绍 Kivy 检查器,这是一个简单的调试界面的工具。为了启动检查器,您运行以下命令:python kivyplayer.py –m inspector。一开始您可能不会注意到任何区别,但如果您按下Ctrl + E,屏幕底部将出现一个栏,就像以下图像左图中的那样:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_06_06.jpg

如果我们按下移至顶部按钮(从左到右数第一个),则栏将移动到屏幕顶部,正如您在右图中所见,这对于我们的特定应用来说是一个更方便的位置。第二个按钮检查激活或关闭检查器行为。我们现在可以通过点击来高亮显示组件。

例如,如果您点击播放/暂停按钮,视频将不会播放;相反,按钮将以红色高亮显示,正如您在下面的左图中所见:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_06_07.jpg

此外,如果我们想可视化当前高亮的部件,我们只需按下父级按钮(从左到右数第三个)。在右图中,您可以看到控制栏播放/暂停按钮的父级)被高亮显示。您还应该注意,长按钮(从左到右数第四个)显示了高亮实例所属的类。如果我们点击此按钮,将显示该部件的所有属性列表,如以下左图所示:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_06_08.jpg

最后,当我们选择一个属性时,我们能够修改它。例如,在右图中,我们修改了控制栏的宽度属性,我们可以看到控制栏立即调整到这些变化。

请记住,由于 Kivy 部件尽可能简单,这意味着很多时候它们是不可见的,因为更复杂的图形显示意味着不必要的过载。然而,这种行为使得我们难以在 GUI 中找到错误。所以当我们的界面没有显示我们期望的内容时,检查器就变得非常有用,帮助我们理解 GUI 的底层树结构。

ActionBar – 一个响应式栏

Kivy 1.8.0 引入了一套新的小部件,它们都与 ActionBar 小部件相关。这个小部件类似于 Android 的操作栏。这不仅会给您的应用程序带来现代和专业的外观,而且还包括更多细微的特性,如对小型屏幕的响应性。根据 ActionBar 小部件的层次结构和组件,不同的小部件将折叠以适应设备中可用的屏幕空间。首先,让我们看看我们计划中的 ActionBar 的最终结果:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_06_09.jpg

我们将 Kivy 语言代码添加到新文件 kivyplayer.kv 中,如下所示:

302\. # File name: kivyplayer.kv
303\. 
304\. <KivyPlayer>:
305\.     list_button: _list_button
306\.     action_bar: _action_bar
307\.     video_controller: _video_controller
308\. 
309\.     VideoController:
310\.         id: _video_controller
311\.         on_playing: root.hide_bars(*args)
312\. 
313\.     ActionBar:
314\.         id: _action_bar
315\.         top: root.height
316\.         ActionView:
317\.             use_separator: True
318\.             ActionListButton:
319\.                 id: _list_button
320\.                 root: root
321\.                 title: 'KPlayer'
322\.             ActionToggleButton:
323\.                 text: 'Mute'
324\.                 on_state: root.toggle_mute(*args)
325\.             ActionGroup:
326\.                 text: 'More Options...'
327\.                 ActionButton:
328\.                     text: 'Open List'
329\.                     on_release: root.show_load_list()
330\.                 ActionTextInput: 
331\.                     on_text_validate: root.search(self.text)

之前代码的层次结构很复杂,因此它也以以下图的形式呈现:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_06_10.jpg

如您在前面的图中所见,KivyPlayer 包含两个主要组件,我们在上一节中创建的 VideoController 以及 ActionBar。如果您还记得,我们为 VideoController 创建了 playing 属性(第 219 行),并将其关联事件 on_playing 绑定到 hide_bars 方法(第 311 行),该方法将基本隐藏操作栏。现在,让我们将注意力集中在 ActionBar 的层次结构上。

ActionBar 总是包含一个 ActionView。在这种情况下,我们添加了一个包含三个小部件的 ActionViewActionListButtonActionToggleButtonActionGroup。所有这些小部件都继承自 ActionItem

注意

ActionView 应该只包含继承自 ActionItem 的小部件。我们可以通过继承 ActionItem 来创建自己的操作项。

ActionGroupActionItem 实例分组,以便组织响应式显示。在这种情况下,它包含一个 ActionButton 实例和一个 ActionTextInput 实例。ActionListButtonActionTextInput 是我们需要创建的个性化小部件。ActionListButton 将继承自 ActionPreviousToggleButtonBehaviour,而 ActionTextInput 继承自 TextInputActionItem

在继续之前,代码中有几个新的属性值得解释。ActionViewuse_separator 属性(第 317 行)表示是否在每个 ActionGroup 前使用分隔符。title 属性(第 321 行),它在 ActionListButton 的组件中显示标题,是从 ActionPrevious 继承的。ActionPrevious 只是一个带有一些额外 GUI 特性(如标题,还可以通过 app_icon 修改 Kivy 图标)的按钮,但更重要的是,它的父级(ActionView)将使用 action_previous 属性保留对其的引用。

现在我们来看看 actiontextinput.kv 文件中 ActionTextInput 的定义:

332\. # File name: actiontextinput.kv
333\. <ActionTextInput@TextInput+ActionItem>
334\.     background_color: 0.2,0.2,0.2,1
335\.     foreground_color: 1,1,1,1
336\.     cursor_color: 1,1,1,1
337\.     hint_text: 'search' 
338\.     multiline: False
339\.     padding: 14
340\.     size_hint: None, 1

正如我们之前所说,ActionTextInput继承自**TextInputActionItemTextInput小部件是一个简单的显示文本输入字段的小部件,用户可以在其中写入。它直接从Widget类和 Kivy 1.9.0 中引入的FocusBehaviour**类继承。我们使用的多重继承表示法(第 333 行)对我们来说是新的。

注意

为了在 Kivy 语言中使用多重继承,我们使用表示法<DerivedClass@BaseClass1+BaseClass2>

**TextInput**小部件是 Kivy 中最灵活的小部件之一,它包含许多可以用来配置它的属性。我们使用了background_colorforeground_colorcursor_color属性(第 334 至 336 行)来分别设置背景、前景和光标颜色。hint_text属性将显示提示背景文本,当TextInput获得焦点时(例如,当我们点击或触摸它时)将消失。multiline属性将指示TextInput是否接受多行,并且当按下Enter键时,它将激活on_text_validate事件,我们在kivyplayer.kv文件(第 331 行)中使用它。

注意,我们还向KivyPlayer(第 305 至 307 行)添加了一些引用。我们在KivyPlayer的 Python 端使用这些引用,即kivyplayer.py。我们将在三个片段中介绍这段代码:

341\. # File name: kivyplayer.py (Fragment 1 of 3)
342\. from kivy.app import App
343\. from kivy.uix.floatlayout import FloatLayout
344\. from kivy.animation import Animation
345\. from kivy.uix.behaviors import ToggleButtonBehavior
346\. from kivy.uix.actionbar import ActionPrevious
347\. 
348\. from kivy.lang import Builder
349\. 
350\. import videocontroller
351\. 
352\. Builder.load_file('actiontextinput.kv')
353\. 
354\. 
355\. class ActionListButton(ToggleButtonBehavior, ActionPrevious):
356\.     pass

在这个片段中,我们添加了所有必要的代码导入。我们还加载了actiontextinput.kv文件,并定义了从ToggleButtonBehaviourActionPrevious继承的ActionListButton类,正如我们之前所指示的。

kivyplayer.py的片段 2 中,我们添加了所有由ActionItems调用的必要方法:

357\. # File name: kivyplayer.py (Fragment 2 of 3)
358\. class KivyPlayer(FloatLayout):
359\. 
360\.     def hide_bars(self, instance, playing):
361\.         if playing:
362\.             self.list_button.state = 'normal'
363\.             self.animationAB = Animation(y=self.height)
364\.             self.action_bar.disabled = True
365\.             self.animationAB.start(self.action_bar)
366\.         else:
367\.             self.action_bar.disabled = False
368\.             self.action_bar.top = self.height
369\.             if hasattr(self, 'animationAB'):
370\.                 self.animationAB.cancel(self.action_bar)
371\. 
372\.     def toggle_mute(self, instance, state):
373\.         if state == 'down':
374\.             self.video_controller.video.volume = 0
375\.         else:
376\.             self.video_controller.video.volume = 1
377\. 
378\.     def show_load_list(self):
379\.         pass
380\. 
381\.     def search(self, text):
382\.         pass

对于本节,我们只是实现了hide_barstoggle_mute方法。hide_bars方法(第 360 至 371 行)在视频播放时以类似我们之前隐藏控制栏的方式隐藏操作栏toggle_button方法(第 372 至 382 行)使用**volume**属性在满音量和静音状态之间切换。代码的片段 3 只包含运行代码的最终命令:

383\. # File name: kivyplayer.py (Fragment 3 of 3)
384\. class KivyPlayerApp(App):
385\.     def build(self):
386\.         return KivyPlayer()
387\. 
388\. if __name__=="__main__":
389\.     KivyPlayerApp().run()

您现在可以再次运行应用程序。您可能想要调整窗口大小,看看操作栏如何根据屏幕大小重新组织组件。以下是中等(左侧)和小型(右侧)尺寸的两个示例:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_06_11.jpg

LoadDialog – 显示文件目录

在本节中,我们将讨论如何在 Kivy 中显示目录树以选择文件。首先,我们将在loaddialog.kv中定义界面:

390\. # File name: loaddialog.kv
391\. <LoadDialog>:
392\.     BoxLayout:
393\.         size: root.size
394\.         pos: root.pos
395\.         orientation: "vertical"
396\.         FileChooserListView:
397\.             id: filechooser
398\.             path: './'
399\.         BoxLayout:
400\.             size_hint_y: None
401\.             height: 30
402\.             Button:
403\.                 text: "Cancel"
404\.                 on_release: root.cancel()
405\.             Button:
406\.                 text: "Load"
407\.                 on_release: root.load(filechooser.path, filechooser.selection)

这段代码中除了使用了**FileChooserListView控件之外,没有其他新内容。它将显示文件的目录树。path属性(第 398 行)将指示开始显示文件的基准路径。除此之外,我们还添加了取消**(第 402 行)和加载按钮(第 405 行),它们调用定义在loaddialog.py文件中的LoadDialog类的相应函数:

408\. # File name: loaddialog.py
409\. 
410\. from kivy.uix.floatlayout import FloatLayout
411\. from kivy.properties import ObjectProperty
412\. from kivy.lang import Builder
413\. 
414\. Builder.load_file('loaddialog.kv')
415\. 
416\. class LoadDialog(FloatLayout):
417\.     load = ObjectProperty(None)
418\.     cancel = ObjectProperty(None)

在这个类定义中实际上没有明确定义的参数,只有几个属性。我们将在kivyplayer.py文件中将方法分配给这些属性,Kivy/Python 将分别调用它们:

419\.     def show_load_list(self):
420\.         content = LoadDialog(load=self.load_list, cancel=self.dismiss_popup)
421\.         self._popup = Popup(title="Load a file list", content=content, size_hint=(1, 1))
422\.         self._popup.open()
423\. 
424\.     def load_list(self, path, filename):
425\.         pass
426\. 
427\.     def dismiss_popup(self):
428\.         self._popup.dismiss() 

如果你记得,ActionBar实例的打开列表按钮调用show_load_list方法(第 329 行)。这个方法将创建一个LoadDialog实例(第 420 行),并将两个其他方法作为构造函数的参数发送:load_list(第 424 行)和dismiss_popup(第 427 行)。这些方法将被分配给loadcancel属性。一旦实例创建完成,我们将在Popup中显示它(实例行 421 和 422)。

现在,当我们在LoadDialog加载按钮(第 420 行)上点击时,将调用load_list方法,当按下取消按钮时,将调用dismiss_popup方法。不要忘记在kivyplayer.py中添加相应的导入:

429\. from kivy.uix.popup import Popup
430\. from loaddialog import LoadDialog
431\. from sidebar import ListItem

下面是生成的截图,我们可以欣赏到树状目录:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_06_12.jpg

ScrollView – 显示视频列表

在本节中,我们将展示在 TED 视频网站上执行搜索的结果,这些结果将显示在一个可以上下滚动的侧边栏中,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_06_13.jpg

让我们在sidebar.kv文件中开始定义侧边栏的组件:

432\. # File name: sidebar.kv
433\. <ListItem>:
434\.     size_hint: [1,None]
435\.     height: 70
436\.     group: 'listitem'
437\.     text_size: [self.width-20, None]
438\. 
439\. 
440\. <Sidebar@ScrollView>:
441\.     playlist: _playlist
442\.     size_hint: [None, None]
443\.     canvas.before:
444\.         Color:
445\.             rgba: 0,0,0,.9
446\.         Rectangle:
447\.             pos: 0,0,
448\.             size: self.width,self.height
449\. 
450\.     GridLayout:
451\.         id: _playlist
452\.         size_hint_y: None
453\.         cols: 1

ListItem类继承自ToggleButtontext_size属性将为文本设置边界。如果视频的标题过长,将使用两行显示。Sidebar类继承自**ScrollView**,这将允许滚动视频列表,类似于我们在上一节LoadDialog中滚动文件的方式。Sidebar内部的GridLayout实例是实际包含和组织ListItem实例的控件。这通过Sidebarplaylist属性(第 442 行)进行引用。

小贴士

ScrollView内部的包含元素必须允许比ScrollView更大,以便可以滚动。如果你想添加垂直滚动,将size_hint_y设置为None;如果你想添加水平滚动,将size_hint_x设置为None

让我们继续在 Python 文件(sidebar.py)中定义侧边栏的定义:

454\. # File name: sidebar.py
455\. 
456\. import json
457\. 
458\. from kivy.uix.togglebutton import ToggleButton
459\. from kivy.properties import ObjectProperty
460\. from kivy.lang import Builder
461\. 
462\. Builder.load_file('sidebar.kv')
463\. 
464\. class ListItem(ToggleButton):
465\.     video = ObjectProperty(None)
466\. 
467\.     def __init__(self, video, meta, surl, **kwargs):
468\.         super(self.__class__, self).__init__(**kwargs)
469\.         self.video = video
470\.         self.meta = meta
471\.         self.surl = surl
472\. 
473\.     def on_state(self, instance, value):
474\.         if self.state == 'down':            
475\.             data = json.load(open(self.meta))['talk']
476\.             self.video.surl = self.surl
477\.             self.video.source = data['media']['internal']['950k']['uri']
478\.             self.video.image = data['images'][-1]['image']['url']

此文件提供了ListItem类的实现。构造函数中有三个参数(第 473 行):一个video小部件的实例、包含视频元数据的meta文件名,这些元数据由 TED 视频提供,以及包含字幕 URL 的surl。当ListItem小部件的state属性发生变化时,会调用on_state方法(第 474 行)。此方法将打开 TED 提供的 JSON 格式的文件,并提取更新视频小部件属性所需的信息。我们在本节代码中包含了一个 TED 元数据文件集合,位于结果文件夹中,以便在您包含自己的 API 之前测试代码。例如,results/97.json包含我们迄今为止使用的丹·吉尔伯特视频的元数据。您可以在该字幕文件的第 477 行和第 478 行验证 JSON 结构。

现在,我们需要在kivyplayer.kv文件中将一个Sidebar实例添加到KivyPlayer中:

479\. # File name: kivyplayer.kv
480\. <KivyPlayer>:
481\.     list_button: _list_button
482\.     action_bar: _action_bar
483\.     video_controller: _video_controller
484\.     side_bar: _side_bar
485\.     playlist: _side_bar.playlist
486\. 
487\.     VideoController:
488\.         id: _video_controller
489\.         control_bar_width: root.width - _side_bar.right
490\. 
491\. (...)
492\. 
493\.     Sidebar:
494\.         id: _side_bar
495\.         width: min(_list_button.width,350)
496\.         height: root.height - _action_bar.height
497\.         top: root.height - _action_bar.height
498\.         x: 0 - self.width if _list_button.state == 'normal' else 0

我们已添加Sidebar实例并基于屏幕上的其他元素定义了一些position属性(第 495 行至第 498 行)。我们还调整了控制栏widthside_bar(第 480 行)。当Sidebar显示时,则控制栏将自动调整到可用空间。我们使用ActionListButton类(第 512 行)控制侧边栏的显示,我们将在kivyplayer.py中定义此类:

499\. # File name: kivyplayer.py
500\. import json
501\. import os
502\. 
503\. (...)
504\. 
505\. from sidebar import ListItem
506\. 
507\. Builder.load_file('actiontextinput.kv')
508\. 
509\. _surl = 'http://www.ted.com/talks/subtitles/id/%s/lang/en'
510\. _meta = 'results/%s.json'
511\. 
512\. class ActionListButton(ToggleButtonBehavior, ActionPrevious):
513\.     def on_state(self, instance, value):
514\.         if self.state == 'normal':
515\.             self.animationSB = Animation(right=0)
516\.             self.animationSB.start(self.root.side_bar)
517\.         else:
518\.             self.root.side_bar.x=0
519\. 
520\. class KivyPlayer(FloatLayout):
521\. 
522\.     def __init__(self, **kwargs):
523\.         super(self.__class__, self).__init__(**kwargs)
524\.         self.playlist.bind(minimum_height= self.playlist.setter('height'))

侧边栏的动画与我们本章中看到的类似。我们还包含了两个全局变量:_surl_meta(第 509 行和第 510 行)。这些字符串将作为字幕和元数据文件的模板。请注意,字符串中的%s将被替换。我们还向KivyPlayer类定义中引入了一个构造函数(__init__)(第 522 行和第 524 行)。第 524 行是必要的,以确保GridLayout实例(在ScrollView内部)适应其高度,从而允许滚动。

现在,我们需要将ListItem实例添加到Sidebar小部件中。为此,我们将在kivyplayer.py中定义load_list方法(第 525 行)和load_from_json方法(第 532 行):

525\.     def load_list(self, path, filename):
526\.         json_data=open(os.path.join(path, filename[0]))
527\.         data = json.load(json_data)
528\.         json_data.close()
529\.         self.load_from_json(data)
530\.         self.dismiss_popup()
531\. 
532\.     def load_from_json(self, data):
533\.         self.playlist.clear_widgets()
534\.         for val in data['results']:
535\.             t = val['talk']
536\.             video = self.video_controller.video
537\.             meta = _meta % t['id']
538\.             surl = _surl % t['id']
539\.             item = ListItem(video, meta, surl, text=t['name'])
540\.             self.playlist.add_widget(item)
541\.         self.list_button.state = 'down'

我们包含了一个 results.json 文件,其中包含从 TED 网站获取的示例搜索结果列表。这个结果以 JSON 格式呈现,您可以在文件中查看。我们需要打开这个文件,并在 侧边栏 中显示其内容。为了做到这一点,我们使用 LoadDialog 显示选择 result.json 文件,并使用 打开列表 按钮进行选择。一旦选择,就会调用 load_list 方法。该方法打开数据并加载 JSON 数据(第 527 行)。一旦加载,它就会调用 load_from_json 方法(第 528 行)。在这个方法中,我们为从 TED 网站搜索得到的结果创建一个 ListItem 实例(第 539 行),并将这些实例添加到播放列表中(即 侧边栏 内的 GridLayout 实例,第 451 行)。第 537 行和 538 行是 Python 中连接字符串的常见方式。它将字符串中存在的 %s 替换为 % 后面的相应参数。现在,当我们打开 results.json 文件时,就像本节开头截图所示,我们将在应用程序中以侧边栏列表的形式看到结果。

搜索 – 查询 TED 开发者 API

本节最后将介绍一些代码更改,以便我们能够搜索 TED 网站。

小贴士

您需要做的第一件事是从 TED 网站使用以下链接获取 API 密钥:

developer.ted.com/member/register

TED API 密钥是一个字母数字编号(例如 '1a3bc2'),它允许您直接查询 TED 网站,并获取我们上一节中使用的 JSON 格式的请求。一旦您在电子邮件账户中收到 API 密钥,您就可以修改 kivyplayer.py 文件,并将其放入 _api 全局变量中。目前,我们可以在 kivyplayer.py 文件中使用如下占位符:

_api = 'YOUR_API_KEY_GOES_HERE'

此外,在 kivyplayer.py 文件中,我们需要引入一个包含搜索模板(search)的全局变量,并替换 _meta 全局变量的内容:

_search = 'https://api.ted.com/v1/search.json?q=%s&categories=talks&api-key=%s'
_meta = 'https://api.ted.com/v1/talks/%s.json?api-key=%s'

注意,现在 _meta 变量有两个 %。因此,我们需要在 load_from_json 方法(第 533 行)中将 meta = meta % t['id'] 代码行替换为 meta = _meta % (t['id'], _api)。另外,由于我们不再打开文件,我们还需要修改 ListItem 类中加载 JSON 的方式,因为我们现在没有文件,而是一个 URL。首先,我们需要在 sidebar.py 文件的开始处导入 URLRequest 类(from kivy.network.urlrequest import UrlRequest),然后修改 on_state 方法以使用 URLRequest 类,就像我们学习字幕时做的那样:

542\.     def on_state(self, instance, value):
543\.         if self.state == 'down':
544\.             req = UrlRequest(self.meta, self.got_meta)
545\.         
546\.     def got_meta(self, req, results):
547\.         data = results['talk']
548\.         self.video.surl = self.surl
549\.         self.video.source = data['media']['internal']['950k']['uri']
550\.         self.video.image = data['images'][-1]['image']['url'] 

我们还需要在 kivyplayer.py 中导入 URLRequest 类,以便在 KivyPlayer 类定义中实现 search 方法:

551\.     def search(self, text):
552\.         url = _search % (text, _api)
553\.         req = UrlRequest(url, self.got_search)
554\. 
555\.     def got_search(self, req, results):
556\.         self.load_from_json(results) 

现在,您可以检查是否收到了 TED API 密钥。一旦您替换了 _api 变量,您将能够使用操作栏中的搜索框查询 TED API。您现在可以使用 ActionTextInput 进行搜索:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-inter-app-gm-py/img/B04244_06_14.jpg

小贴士

请记住,您刚刚创建的 API 密钥可以识别您和您的应用程序作为 TED 网站的用户。所有通过该 API 注册的活动都是您的责任。您不应该将此 API 密钥告诉任何人。

控制您的 API 密钥的使用涉及设置自己的服务器,其中 API 密钥被安全存储。该服务器将作为您应用程序的代理 (en.wikipedia.org/wiki/Proxy_server),并应限制查询。例如,它应避免大量查询等滥用行为。

摘要

在本章中,我们创建了一个集成了许多 Kivy 组件的应用程序。我们讨论了如何控制视频以及如何将屏幕的不同元素与之关联。我们探索了不同的 Kivy 小部件,并实现了复杂的交互来显示可滚动的元素列表。以下是本章中我们使用的新类和组件列表:

  • Video: 从 Image 继承的 allow_stretchsource 属性;stateprogress 属性;_on_loadon_eoson_sourceon_stateon_positionseek 方法

  • AsyncImage: 从 Image 继承的 source 属性;从 Widget 继承的 opacity 属性

  • Label: texture_size 属性

  • Slider: valuemax 属性
Touch: double_tap

  • Kivy 检查器类

  • ActionBarActionViewActionItemActionPreviousActionToggleButtonActionGroupActionButton 类,以及 ActionViewuse_separator 属性和 ActionPrevious 的标题属性

  • Textinput: background_colorforeground_colorcursor_colormultiLine 属性

  • FileChooserListView: path 属性

  • ScrollView

作为本章的副产品,我们获得了一个经过优化的 Video 小部件,我们可以在其他应用程序中使用它。这个 Video 小部件将我们以 JSON 格式文件接收的子标题与视频的进度同步,并具有响应式的 控制栏

我们已经掌握了 Video 小部件的使用。我们学习了如何控制其进度并添加字幕。我们还介绍了如何查询 TED 开发者 API 以获取结果列表,并练习了操作 JSON 格式的技能。我们还学习了如何使用 Kivy 调试器来检测界面中的错误。

我们还努力使我们的 KivyPlayer 应用看起来更专业。我们通过引入隐藏 GUI 组件的动画来优化屏幕的使用,这些动画在不需要时隐藏。在这个过程中,我们使用了许多 Kivy 元素来使我们的小部件保持一致,并审查了诸如行为、工厂、动画、触摸事件以及属性的使用等有趣的主题,以便创建多功能组件。

开始即是结束,因此现在轮到你自己开始自己的应用了。我真心希望,你从这本书中学到的知识能帮助你实现你的想法,并开始你自己的应用。

内容概要:本文详细介绍了“秒杀商城”微服务架构的设计与实战全过程,涵盖系统从需求分析、服务拆分、技术选型到核心功能开发、分布式事务处理、容器化部署及监控链路追踪的完整流程。重点解决了高并发场景下的超卖问题,采用Redis预减库存、消息队列削峰、数据库乐观锁等手段保障数据一致性,并通过Nacos实现服务注册发现与配置管理,利用Seata处理跨服务分布式事务,结合RabbitMQ实现异步下单,提升系统吞吐能力。同时,项目支持Docker Compose快速部署Kubernetes生产级编排,集成Sleuth+Zipkin链路追踪与Prometheus+Grafana监控体系,构建可观测性强的微服务系统。; 适合人群:具备Java基础Spring Boot开发经验,熟悉微服务基本概念的中高级研发人员,尤其是希望深入理解高并发系统设计、分布式事务、服务治理等核心技术的开发者;适合工作2-5年、有志于转型微服务或提升架构能力的工程师; 使用场景及目标:①学习如何基于Spring Cloud Alibaba构建完整的微服务项目;②掌握秒杀场景下高并发、超卖控制、异步化、削峰填谷等关键技术方案;③实践分布式事务(Seata)、服务熔断降级、链路追踪、统一配置中心等企业级中间件的应用;④完成从本地开发到容器化部署的全流程落地; 阅读建议:建议按照文档提供的七个阶段循序渐进地动手实践,重点关注秒杀流程设计、服务间通信机制、分布式事务实现系统性能优化部分,结合代码调试与监控工具深入理解各组件协作原理,真正掌握高并发微服务系统的构建能力。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值