Kivy
Installation
Kivy是由MIT研發的Python視窗程式套件。欲使用之,應先
安裝Python 。此處擬使用pycharm做為開發之IDE,首先自
官網下載pycharm ,請選擇
Community 版本。安裝之後開啟新專案,請選擇File>Settings,然後點選Project下之Python Interpreter,正確選擇上方之Python Interpreter,然後在下方的Package處,點選+,看是否有Kivy可以安裝。若無,請參考
Kivy官網 進行安裝。接著再回到pycharm應可以找到kivy,點選安裝即可。
Kivy Basic
首先在Pycharm建立一個Project,然後加上如下的Code:
from kivy.app import App
from kivy.uix.label import Label
class MyApp (App) :
def build (self) :
return Label(text="Kivy" )
if __name__ == '__main__' :
MyApp().run()
首先我們必須import App class,這是為了讓我們可以建立一個kivy application。這可以視為kivy run loop的主要進入點,當建立app完成後,呼叫run()方法來開始此app。
from kivy.uix.label import Label這行import Label這個widget,大部分的widget包含在uix這個package中。
建立一個繼承自App的class,最重要的是要override build這個方法。有此方法後,呼叫run()即可產生視窗。
Widgets
Widgets是視窗介面的元件,儲存在kivy.uix 這個module內。其中傳統的基礎元件有Label, Button, CheckBox, Image, Slider, Progress Bar, Text Input, Toggle button, Switch, Video 等。上例中我們使用Label來顯示文字,可以直接使用Label內建的方法來修改Label的顯示,例如:
from kivy.app import App
from kivy.uix.label import Label
class MyApp (App) :
def build (self) :
lb = Label()
lb.font_size = '65'
lb.text = "Kivy Label"
lb.color = "red"
lb.text_size = (200 , None )
return lb
if __name__ == '__main__' :
MyApp().run()
也可以使用如下的方法來控制Label內的文字,類似html(此處使用[])來改變文字顏色或其他屬性。Note: markup要等於True。
class MyApp (App) :
def build (self) :
return Label(text="[color=ff00ff]Kivy[/color]\n[color=00ffff]Label[/color]" , markup = True )
Layout
當視窗中有多個元件,此時我們需要配置其對應位置及大小,這個工作由layout來完成。根據之前提到的kivy.uix module,可以看到layouts也是屬於一種widget,其中提供了
Anchor Layout, Box Layout, Float Layout, Grid Layout, PageLayout, Relative Layout, Scatter Layout, Stack Layout 等類型。此處先使用Grid Layout來練習。
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
class MyApp (App) :
def build (self) :
lb1 = Label(text="Label 1" )
lb2 = Label(text="[color=ff00ff]Kivy[/color]\n[color=00ffff]Label[/color]" , markup = True )
lb3 = Label(text="Label 3" )
lb4 = Label(text="Label 4" )
lb5 = Label(text="Label 5" )
layout = GridLayout(cols=2 )
layout.add_widget(lb1)
layout.add_widget(lb2)
layout.add_widget(lb3)
layout.add_widget(lb4)
layout.add_widget(lb5)
return layout
if __name__ == '__main__' :
MyApp().run()
上例中,因為要使用Grid Layout來配置元件,所以首先需要先import GridLayout。
使用cols=2參數來設定Grid的欄位數。並使用add_widget()方法來加入元件。
也可以將layout分開為另一個class,寫法如下:
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
class MyLayout (GridLayout) :
def __init__ (self, **kwargs) :
super().__init__(**kwargs)
lb1 = Label(text="Label 1" )
lb2 = Label(text="[color=ff00ff]Kivy[/color]\n[color=00ffff]Label[/color]" , markup=True )
lb3 = Label(text="Label 3" )
lb4 = Label(text="Label 4" )
lb5 = Label(text="Label 5" )
self.cols = 2
self.add_widget(lb1)
self.add_widget(lb2)
self.add_widget(lb3)
self.add_widget(lb4)
self.add_widget(lb5)
class MyApp (App) :
def build (self) :
return MyLayout()
if __name__ == '__main__' :
MyApp().run()
分開寫的好處是讓我們更容易組織我們的工作。接下來在其內再加上一個button,將其至於最下方,為了將其與之前的label分開,我們將GridLayout分為兩個部分,Labels放在一個上層的GridLayout內,而新加入的button則與上層layout並列。如下:
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
class MyLayout (GridLayout) :
def __init__ (self, **kwargs) :
super().__init__(**kwargs)
self.cols = 1
self.topFrame = GridLayout()
self.topFrame.cols = 2
lb1 = Label(text="Label 1" )
lb2 = Label(text="Label 2" )
lb3 = Label(text="Label 3" )
lb4 = Label(text="Label 4" )
lb5 = Label(text="Label 5" )
self.topFrame.add_widget(lb1)
self.topFrame.add_widget(lb2)
self.topFrame.add_widget(lb3)
self.topFrame.add_widget(lb4)
self.topFrame.add_widget(lb5)
self.add_widget(self.topFrame)
bt = Button(text="Click" )
self.add_widget(bt)
class MyApp (App) :
def build (self) :
return MyLayout()
if __name__ == '__main__' :
MyApp().run()
bind()
有了按鈕,原則上點擊後我們期望他觸發事件,此時可以使用bind()方法來連結方法或函數。如下:
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
class MyLayout (GridLayout) :
def __init__ (self, **kwargs) :
super().__init__(**kwargs)
self.count = 0
self.cols = 1
self.topFrame = GridLayout()
self.topFrame.cols = 2
self.lb1 = Label(text="Label 1" )
self.lb2 = Label(text="Label 2" )
self.lb3 = Label(text="Label 3" )
self.lb4 = Label(text="Label 4" )
self.lb5 = Label(text="Label 5" )
self.lb6 = Label(text=f"{self.count}" )
self.topFrame.add_widget(self.lb1)
self.topFrame.add_widget(self.lb2)
self.topFrame.add_widget(self.lb3)
self.topFrame.add_widget(self.lb4)
self.topFrame.add_widget(self.lb5)
self.topFrame.add_widget(self.lb6)
self.add_widget(self.topFrame)
self.bt = Button(text="Click" )
self.bt.bind(on_press=self.pressed)
self.add_widget(self.bt)
def pressed (self, instance) :
self.count += 1
print(self.count, instance.text)
self.lb6.text = f"{self.count}"
class MyApp (App) :
def build (self) :
return MyLayout()
if __name__ == '__main__' :
MyApp().run()
Kv Language
Kivy提供kv language可以讓我們將widget的架構分開撰寫,有點類似CSS。做法是同時開啟一個新檔案,使用跟class app同名的名稱(若是名稱為...App,需省略App),延伸檔名為kv。例如上例中為MyApp,所以我們須建立新檔案為my.kv(名稱使用小寫英文字母)。
首先將主程式(.py)的內容設定如下:
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
class MyLayout (Widget) :
pass
class MyApp (App) :
def build (self) :
return MyLayout()
if __name__ == '__main__' :
MyApp().run()
須注意要加上
from kivy.uix.widget import Widget 這個import,並將class MyLayout改為繼承Widget。接著在my.kv內加上以下內容:
<MyLayout>
Label:
text: "Label 1"
接著執行程式,可以看到視窗中包含一個Label。
首先class的名稱,需要加上<>符號。Widget跟屬性之後使用:。
一樣要有Indentation來建立架構。
接下來把my.kv內容增加如下:
<MyLayout>
GridLayout:
cols: 1
Label:
text: "Label 1"
Button:
text: "Click"
GridLayout:
cols: 2
Label:
text: "Label 2"
Label:
text: "Label 3"
Label:
text: "Label 4"
Label:
text: "Label 5"
TextInput:
multiline: False
執行程式後,可以看到其中有我們在my.kv內加入的元件(widget)以及所使用的layout。這個方式很顯然可以讓我們很容易地在.kv檔案內完成我們的版面配置,並可進行快速地調整修改或更新。
上例中的元件會擠在左下角,我們可以override size來使用全視窗,並將內容作稍許修改如下:
<MyLayout>
GridLayout:
cols: 1
size: root.width, root.height
Label:
text: "Label 1"
Button:
text: "Click"
GridLayout:
rows: 2
Label:
text: "Label 2"
Label:
text: "Label 3"
Label:
text: "Label 4"
Label:
text: "Label 5"
GridLayout:
cols: 1
TextInput:
multiline: False
此時我們可以得到我們預期中的效果。因為我們將所有元件安排為樹狀結構,root表示整個視窗,root.width&root.height則表示整個視窗的寬高,你可以縮放視窗的大小看結果。甚至可以讓此二參數各-100,可以看到上方及右方縮減了,因為kivy的視窗原點為左下角,所以寬高由左下角往右及往上計算。
properties & bind
重新修改.py檔案,在class MyLayout其中加入兩個變數稱為name與age,如下:
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
class MyLayout (Widget) :
name = ObjectProperty(None )
age = ObjectProperty(None )
def clickBtn (self) :
print(f"Name: {self.name.text}\nAge: {self.age.text}" )
self.name.text= ""
self.age.text= ""
def inputName (self) :
print("Input name......" )
def inputAge (self) :
print("Input age......" )
class MyApp (App) :
def build (self) :
return MyLayout()
if __name__ == '__main__' :
MyApp().run()
因為要與.kv檔案內容呼應,無法像一般python的寫法直接定義變數,此處須導入property,可以自動更新自.kv處取得之變數值。使用時,必須宣告於class level,而不是宣告於任何的方法內(亦即不為method variable)。
除了變數之外,同時加入三個方法,其中self.name.text以及self.age.text指TextInput內的文字。
接著在my.kv內加入程式碼如下:
<MyLayout>
name: name
age: age
GridLayout:
cols: 1
size: root.width, root.height
GridLayout:
cols: 2
Label:
text: "Name:"
TextInput:
id: name
hint_text: "input your name......"
multiline: False
on_focus: root.inputName()
Label:
text: "Age:"
TextInput:
id: age
hint_text: "input your age......"
multiline: False
on_focus: root.inputAge()
Button:
text: "Click"
on_press: root.clickBtn()
如此可以得到我們需要的視窗配置。在my.kv內的<MyLayout> 內,一樣定義name與age兩個變數用來呼應.py內的property。在對應的widget內,可以使用on_ 的事件(Event)觸發方法(e.g. on_press、on_text、on_mouseover、on_focus )來觸發對應方法。要注意的是當要呼叫MyLayout內的方法,需要使用root來作為前綴詞(root.methodName()) ,方可呼叫該方法(如前所述,root表示該視窗)。
BoxLayout & styles
現在嘗試使用kv language來實現boxlayout以及一些元件的styling方式。boxlayout是將元件做垂直或是水平方向的排列,預設方式為水平向,使用orientation參數。先看如下範例,首先.py檔案的程式碼如下:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
class Root (Widget) :
def rootMethod (self) :
print("Root method" )
class ALabel (Label) :
def __init__ (self, **kwargs) :
super().__init__()
self.color = "blue"
class AButton (Button) :
def __init__ (self, **kwargs) :
super().__init__()
self.color = "red"
def clickbt (self) :
print("Button is clicked." )
class MyApp (App) :
def build (self) :
return Root()
if __name__ == '__main__' :
MyApp().run()
此處除了之前的class之外,額外設計了ALabel以及AButton兩個class,此二元件分別繼承自Label以及Button。接下來先看.kv檔案的程式碼,如下:
<Button>
background_color: (0.5 , 0.2 , 0.3 , 1 )
outline_color: "olive"
outline_width: 3
<Root>
BoxLayout:
padding: 12
spacing: 12
size: root.width, root.height
orientation: "vertical"
BoxLayout:
orientation: "horizontal"
padding: 10
spacing: 10
Label:
text: "Name:"
bold: True
italic: True
color: "orange"
background_color: (1 ,1 ,0 ,1 )
canvas.before:
Color:
rgba: self.background_color
Rectangle:
size: self.size
pos: self.pos
TextInput:
id: name
hint_text: "input your name......"
multiline: False
ALabel:
text: "Age:"
font_size: 32
TextInput:
id: age
hint_text: "input your age......"
multiline: False
Button:
text: "Click"
on_press: root.rootMethod()
AButton:
text: "Click Button"
on_press: self.clickbt()
範例中共有六個元件(兩個Label、兩個TextInput、以及兩個Button),此處使用BoxLayout來布置,Label與TextInpu由一個BoxLayout布置(水平排列),再將此boxlayout與兩個Button放置於另一個boxlayout內(垂直排列)。改變style的寫法,可以約類分為以下三類:
定義一個元件標籤,例如在最上方設計了一個<Button>物件的style,有點類似CSS的做法,此處定義的style將影響之後所有的Button。
在各自的元件內加入想要使用的style,例如在第一個Label內的bold等相關參數。
建立新的class,例如ALabel以及AButton(因為繼承自Label及Button,原則上就是Label&Button),在建構子內描繪style,之後在.kv檔內使用此元件即可(e.g. ALabel & AButton in .kv)。class內可以定義方法,在.kv中使用self.前綴 來呼叫該方法即可,例如self.click()。
Touch
Kivy可以直接偵測滑鼠在其中的位置,事件名稱為on_touch_down、on_touch_up、以及on_touch_move,當牽扯到滑鼠位置時使用,使用下例說明,首先.py檔案內容如下:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.graphics import Color, Rectangle
class Root (Widget) :
pass
class ALabel (Label) :
def __init__ (self, **kwargs) :
super().__init__()
self.color = "black"
with self.canvas:
Color(0 , .5 , .5 , 1 )
self.rect = Rectangle(pos=self.pos, size=self.size)
self.bind(pos=self.updateRect, size=self.updateRect)
def updateRect (self, instance, value) :
instance.rect.pos = instance.pos
instance.rect.size = instance.size
def on_touch_down (self, touch) :
if self.collide_point(*touch.pos):
self.font_size = 32
self.color = "red"
return super().on_touch_down(touch)
def on_touch_up (self, touch) :
if self.collide_point(*touch.pos):
self.font_size = 16
self.color = "black"
self.outline_color = "olive"
return super().on_touch_up(touch)
class AButton (Button) :
def __init__ (self, **kwargs) :
super().__init__()
self.color = "blue"
self.background_color = (1 , 1 , 0 , 1 )
def on_touch_down (self, touch) :
if self.collide_point(*touch.pos):
self.background_color = (.5 , .5 , 0 , .5 )
self.opacity = .3
self.color = "red"
def on_touch_up (self, touch) :
if self.collide_point(*touch.pos):
self.background_color = (1 , 1 , 0 , 1 )
self.opacity = 1
self.color = "blue"
def on_touch_move (self, touch) :
if self.collide_point(*touch.pos):
print(f"position: {touch}" )
class MyApp (App) :
def build (self) :
return Root()
if __name__ == '__main__' :
MyApp().run()
類似之前的例子,這裡分別設計ALabel & AButton兩個class來建立新的Label&Button,裡面包含on_touch_down()、on_touch_up()、on_touch_move()等方法。
使用self.collide_point(*touch.pos)方法來判斷是否滑鼠座標與widget範圍發生collide,也就是判斷是否滑鼠進入該widget範圍。
若要修改Label的背景顏色,需要修改其canvas顏色,而Button可以直接修改background_color參數的值即可。
接下來看.kv的檔案內容:
<Root>
BoxLayout:
padding: 12
spacing: 12
size: root.width, root.height
orientation: "vertical"
ALabel:
text: "Label 1"
ALabel:
text: "Label 2"
AButton:
text: "A"
AButton:
text: "B"
AButton:
text: "C"
這部分就沒甚麼好說明的了。
另一個寫法,如下:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.graphics import Color, Rectangle
class Root (Widget) :
pass
class ALabel (Label) :
def __init__ (self, **kwargs) :
super().__init__()
self.color = "pink"
def style1 (self) :
self.color = "green"
self.font_size = 32
def style2 (self) :
self.color = "pink"
self.font_size = 16
class AButton (Button) :
def __init__ (self, **kwargs) :
super().__init__()
self.color = "pink"
def style1 (self) :
self.color = "green"
self.background_color = "olive"
self.font_size = 32
def style2 (self) :
self.color = "pink"
self.background_color = "orange"
self.font_size = 16
class MyApp (App) :
def build (self) :
return Root()
if __name__ == '__main__' :
MyApp().run()
此處的ALabel&AButton各自設計style1與style2方法以供呼叫。
.kv檔案內容:
<Root>
BoxLayout:
padding: 12
spacing: 12
size: root.width, root.height
orientation: "vertical"
Label:
text: "Label 1"
on_touch_down: if self.collide_point(*args[1 ].pos): self.color = "red"
on_touch_up: if self.collide_point(*args[1 ].pos): self.color = "blue"
ALabel:
text: "Label 2"
on_touch_down: if self.collide_point(*args[1 ].pos): self.style1()
on_touch_up: if self.collide_point(*args[1 ].pos): self.style2()
Button:
text: "A"
on_touch_down: if self.collide_point(*args[1 ].pos): self.color = "red"
on_touch_up: if self.collide_point(*args[1 ].pos): self.color = "blue"
AButton:
text: "B"
on_touch_down: if self.collide_point(*args[1 ].pos): self.style1()
on_touch_up: if self.collide_point(*args[1 ].pos): self.style2()
Button:
text: "C"
在.kv檔中,直接定義on_touch_down,判斷有collide point時,執行對應程式碼。
Screen Management
Kivy可以使用Screen Manager管理多個螢幕(Screen),預設值是管理一個Screen。我們可以直接在Screen內做配置,如下例:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.uix.screenmanager import Screen
class RootScreen (Screen) :
pass
class MyApp (App) :
def build (self) :
return RootScreen()
if __name__ == '__main__' :
MyApp().run()
要記得導入Screen:
from kivy.uix.screenmanager import Screen
接著在.kv檔案內建立一個簡單的Button,如下:
<RootScreen>
Button:
text: "A Button"
on_release: print(root.__class__)
Screen Manager
上例中只有一個Screen,若是要有多個Screen,則需要使用Screen Manager(也就是需要導入
from kivy.uix.screenmanager import Screen, ScreenManager )。
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.uix.screenmanager import Screen, ScreenManager
class Manager (ScreenManager) :
pass
class MyApp (App) :
def build (self) :
return Manager()
if __name__ == '__main__' :
MyApp().run()
在.kv檔案中,設計兩個Screen,並分別在其中加入button,點擊前往不同的Screen。
<Manager>
Screen:
name: "Home"
BoxLayout:
orientation: "vertical"
Label:
text: "The Home Screen"
Button:
size: 100 , 100
size_hint: 1 , None
text: "To Next Screen"
on_release: root.current = 'Next'
Screen:
name: "Next"
BoxLayout:
orientation: "vertical"
Label:
text: "The Second Screen"
Button:
size: 100 , 100
size_hint: 1 , None
text: "Back to Home"
on_release: root.current = "Home"
Transitions
另一個Screen,可以有多種不同的花樣可做選擇,如下:
NoTransition
SlideTransition
CardTransition
SwapTransition
FadeTransition
WipeTransition
FallOutTransition
RiseInTransition
請自行在以下的程式碼選擇效果測試。
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.uix.screenmanager import Screen, ScreenManager, CardTransition, WipeTransition
class Manager (ScreenManager) :
pass
class Screen1 (Screen) :
pass
class Screen2 (Screen) :
pass
class Screen3 (Screen) :
pass
class MyApp (App) :
def build (self) :
manager = Manager(transition=CardTransition())
return manager
if __name__ == '__main__' :
MyApp().run()
此例中分別將每個Screen各自定義一個class,如此可以對其做各自的操作。
在build方法內instantize Screen Manager物件,並指定transition方法。
在.kv檔案內的程式碼如下:
<Manager>:
Screen1
Screen2
Screen3
<Screen1>:
name: "Home"
BoxLayout:
orientation: "vertical"
Label:
text: "The Home Screen"
Button:
size: 100 , 100
size_hint: 1 , None
text: "To Next Screen"
on_release:
root.manager.transition.direction = 'right'
root.manager.transition.duration = 1
root.manager.current = 'Second'
<Screen2>:
name: "Second"
BoxLayout:
orientation: "vertical"
Label:
text: "The Second Screen"
GridLayout:
size: 100 , 100
size_hint: 1 , None
cols: 2
Button:
text: "Back to Home"
on_release:
root.manager.transition.direction = 'left'
root.manager.transition.duration = 1
root.manager.current = 'Home'
Button:
text: "To Next Screen"
on_release:
root.manager.transition.direction = 'left'
root.manager.transition.duration = 1
root.manager.current = 'Third'
<Screen3>:
name: "Third"
BoxLayout:
orientation: "vertical"
Label:
text: "The Third Screen"
GridLayout:
size: 100 , 100
size_hint: 1 , None
cols: 2
Button:
text: "Back to Home"
on_release:
root.manager.transition.direction = 'left'
root.manager.transition.duration = 1
root.manager.current = 'Home'
Button:
text: "To Pre Screen"
on_release:
root.manager.transition.direction = 'left'
root.manager.transition.duration = 1
root.manager.current = 'Second'
分別建立Screen內容,設計button來觸發換頁。
Clock
clock物件可以讓我們排程函數,可能在未來呼叫一次或是在固定時間間隔持續呼叫,使用之前需先呼叫
from kivy.clock import Clock ,用法如下:
Clock.schedule_interval(callback, 1): 每1秒呼叫一次callback函數
Clock.schedule_once(callback, 5): 5秒後呼叫callback函數
Clock.schedule_onec(callback): 下一個frame呼叫callback函數
設計一個簡單的時鐘例子:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.clock import Clock
from datetime import datetime as dtime
class clock (Label) :
def update (self, *args) :
self.text = dtime.now().strftime("%Y-%B-%d %A %I:%M:%S %p" )
class MyApp (App) :
def build (self) :
root = clock()
Clock.schedule_interval(root.update, 1 )
return root
if __name__ == '__main__' :
MyApp().run()
Another Clock
此處介紹另一個寫法,以及加上cancel()跟schedule_once()等方法。在.py檔案中加入以下程式碼:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
from kivy.clock import Clock
from datetime import datetime as dtime
class MyApp (App) :
def on_start (self) :
self.clock = Clock.schedule_interval(self.update_label, 1 )
Clock.schedule_once(self.stop_interval, 10 )
def stop_interval (self, *args) :
self.clock.cancel()
def update_label (self, *args) :
self.root.ids.time.text = dtime.now().strftime("%Y-%B-%d %A %I:%M:%S %p" )
if __name__ == '__main__' :
MyApp().run()
在.kv檔案中加入元件如下:
GridLayout:
cols: 1
Label:
id: time
text: "0"
Graphics
Graphics模組可讓我們繪製幾何圖形,使用前須匯入kivy.graphics模組,舉例如下:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import *
class Paint (Widget) :
def __init__ (self, **kwargs) :
super().__init__(**kwargs)
with self.canvas:
Color(1 ,0 ,1 ,1 , mode="rgba" )
self.rect = Rectangle(pos=(0 ,0 ), size=(20 , 20 ))
def on_touch_move (self, touch) :
x = touch.pos[0 ]-self.rect.size[0 ]/2
y = touch.pos[1 ]-self.rect.size[1 ]/2
self.rect.pos = (x,y)
class MyApp (App) :
def build (self) :
return Paint()
if __name__ == '__main__' :
MyApp().run()
記得導入from kivy.graphics import *,也可以根據要使用的元件個別導入
再看另一個例子:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import *
from kivy.core.window import Window
import random as rd
class Paint (Widget) :
def __init__ (self, **kwargs) :
super().__init__(**kwargs)
self.size = Window.size
self.nodes = []
self.draw_nodes()
def draw_nodes (self) :
with self.canvas:
Color(rd.random(), rd.random(), rd.random(), rd.random(), mode="rgba" )
for i in range(50 ):
node = Rectangle(pos=(rd.random()*self.width, rd.random()*self.height), size=(5 , 5 ))
self.nodes.append(node)
def on_touch_down (self, touch) :
with self.canvas:
for node in self.nodes:
Line(points=(touch.pos, node.pos))
def on_touch_up (self, touch) :
self.canvas.clear()
self.draw_nodes()
class MyApp (App) :
def build (self) :
return Paint()
if __name__ == '__main__' :
MyApp().run()
此例中先設計一些點(node),散佈在canvas內,使用self.size = Window.size來讓canvas size等於整個視窗。
點擊視窗某一個點會繪製該點到其他每一node之間的線段。若是將此段程式碼放置於on_touch_move()內,則每一個move都會產生線段,會出現一大堆線段。
使用self.canvas.clear()來清除畫面所有繪製元件。
Pygame
basic
Pygame是用來製作遊戲的Python模組,首先先安裝Pygame,在PyCharm的File > Settings之下按+,輸入pygame搜尋然後點擊安裝即可。開啟新Project後,輸入以下程式碼:
import sys
import pygame
from pygame.locals import QUIT
pygame.init()
win = pygame.display.set_mode((800 , 600 ))
pygame.display.set_caption("Hello" )
win.fill((0 , 255 , 255 ))
font = pygame.font.SysFont(None , 60 )
surface = font.render("Hello, World!" , True , (0 ,0 ,0 ))
win.blit(surface, (10 , 10 ))
pygame.display.update()
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
貼圖
跟上例類似的做法,設計一個背景圖,再加上一個隨著游標移動的小圖,程式碼如下:
import sys
import pygame
from pygame.locals import *
background = 'your directory/background.png'
sheep = 'your directory/sheep.png'
pygame.init()
win = pygame.display.set_mode((800 , 600 ))
pygame.display.set_caption("Hello" )
bg = pygame.image.load(background).convert()
sp = pygame.image.load(sheep).convert_alpha()
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
win.blit(bg, (0 , 0 ))
x, y = pygame.mouse.get_pos()
x -= sp.get_width()/2
y -= sp.get_height()/2
win.blit(sp, (x,y))
pygame.display.update()
使用pygame.image.load()來讀取圖片,取得的圖片呼叫covert()可以將圖片轉換成跟display相同格式,如此可以較為快速地顯示因為兩者有相同的depth(number of colors),呼叫covert_alpha()是因為多了alpha information(translucent,不過結果好像沒有半透明,應該原圖就要設計是半透明)。結果顯示如下:
將update()與blit()放置一起,讀取後顯示。
Event
在前例中,我們只處理了一個事件(Event)QUIT,此事件讓我們可以關閉視窗。除了QUIT之外,pygame還可以產生其他的事件。在前例中,我們呼叫pygame.event.get()來萃取所有的事件然後將其自佇列(Queue)中移除。以下條列一些我們可能用到的標準事件(Standard events)。see
https://www.pygame.org/docs/ref/event.html for more details.
QUIT
ACTIVEEVENT
KEYDOWN
KEYUP
MOUSEMOTION
MOUSEBUTTONDOWN
MOUSEBUTTONUP
JOYAXISMOTION
JOYBALLMOTION
JOYHATMOTION
JOYBUTTONDOWN
JOYBUTTONUP
VIDEORESIZE
VIDEOEXPOSE
USEREVENT
WINDOWSHOWN
WINDOWHIDDEN
WINDOWMOVED
WINDOWRESIZED
WINDOWSIZECHANGED
WINDOWMINIMIZED
WINDOWMAXIMIZED
WINDOWRESTORED
WINDOWENTER
WINDOWLEAVE
WINDOWFOCUSGAINED
WINDOWFOCUSLOST
WINDOWCLOSE
WINDOWTAKEFOCUS
WINDOWHITTEST
首先寫一個簡單的程式來顯示所有產生的事件。如下:
import sys
import pygame
from pygame.locals import *
pygame.init()
win = pygame.display.set_mode((800 , 600 ), 0 )
pygame.display.set_caption("Hello" )
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
print(event)
pygame.display.update()
跟之前的程式碼差不多,只是印出event,移動或點擊滑鼠或按下keyboard,我們可以在console看到多個事件顯示。
Mouse Motion&Button Events
滑鼠的事件主要是移動或是按下按鍵(
buttons ),使用一個tuple來對應三個按鍵,所以buttons[0]、buttons[1]、buttons[2]分別表示左鍵、中鍵(滾輪)、以及右鍵。當button被按下,其值為1,否則為0。另一個值是滑鼠位置(
pos ),也是使用tuple儲存滑鼠位置(x,y)。此外還有
rel 屬性,也是使用tuple來儲存滑鼠自上次到此次所移動的距離。
import sys
import pygame
from pygame.locals import *
pygame.init()
win = pygame.display.set_mode((800 , 600 ), 0 )
pygame.display.set_caption("Hello" )
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == MOUSEMOTION:
print("distance moved:" , event.rel, "pos:" , event.pos, "buttons:" , event.buttons)
if event.type == MOUSEBUTTONDOWN:
print("button:" , event.button, "pos:" , event.pos)
pygame.display.update()
在測試時可以一次顯示一種事件,以免顯示太多顯得混亂。
Keyboard Events
使用KEYDOWN表示按鍵被按下(press),KEYUP表示放開(release)按鍵。試試看以下的例子:
import sys
import pygame
from pygame.locals import *
sp = 'sheep.png'
bg = 'background.png'
pygame.init()
win = pygame.display.set_mode((800 , 600 ), 0 )
pygame.display.set_caption("Moving Sheep" )
sheep = pygame.image.load(sp).convert()
background = pygame.image.load(bg).convert()
x, y = 0 , 0
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == KEYDOWN:
if event.key == K_LEFT:
x -= 2
elif event.key == K_RIGHT:
x += 2
elif event.key == K_UP:
y -= 2
elif event.key == K_DOWN:
y += 2
win.blit(background, (0 , 0 ))
win.blit(sheep, (x, y))
pygame.display.update()
我們可以按方向鍵來移動sheep。至於要設計哪些按鍵,請參考
https://www.pygame.org/docs/ref/key.html 。
Keep Moving
上例中,當按下一次按鍵,圖片才會移動一下,要讓圖片一直移動,必須要持續地按按鍵,我們可以使用如下的設計,當我們按住按鍵時,圖片便可持續的移動。
import sys
import pygame
from pygame.locals import *
sp = 'sheep.png'
bg = 'background.png'
pygame.init()
win = pygame.display.set_mode((800 , 600 ), 0 )
pygame.display.set_caption("Moving Sheep" )
sheep = pygame.image.load(sp).convert()
background = pygame.image.load(bg).convert()
x, y = 0 , 0
move_right = False
move_left = False
move_up = False
move_down = False
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == KEYDOWN:
if event.key == K_LEFT:
move_left = True
elif event.key == K_RIGHT:
move_right = True
elif event.key == K_UP:
move_up = True
elif event.key == K_DOWN:
move_down = True
elif event.type == KEYUP:
if event.key == K_LEFT:
move_left = False
elif event.key == K_RIGHT:
move_right = False
elif event.key == K_UP:
move_up = False
elif event.key == K_DOWN:
move_down = False
if move_right:
x += 1
if move_left:
x -= 1
if move_up:
y -= 1
if move_down:
y += 1
win.blit(background, (0 , 0 ))
win.blit(sheep, (x, y))
pygame.display.update()
另一個簡單的寫法:
import sys
import pygame
from pygame.locals import *
sp = 'sheep.png'
bg = 'background.png'
pygame.init()
win = pygame.display.set_mode((800 , 600 ), 0 )
pygame.display.set_caption("Moving Sheep" )
sheep = pygame.image.load(sp).convert()
background = pygame.image.load(bg).convert()
x, y = 0 , 0
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
keys = pygame.key.get_pressed()
if keys[pygame.K_RIGHT]:
x += 1
if keys[pygame.K_LEFT]:
x -= 1
if keys[pygame.K_UP]:
y -= 1
if keys[pygame.K_DOWN]:
y += 1
win.blit(background, (0 , 0 ))
win.blit(sheep, (x, y))
pygame.display.update()
按鍵控制全螢幕
設計一個程式,按下某鍵變成全螢幕,放開後變回原大小。(也可以設計一鍵全螢幕,另一鍵回原大小)
import sys
import pygame
from pygame.locals import *
sp = 'sheep.png'
bg = 'background.png'
pygame.init()
win = pygame.display.set_mode((640 , 480 ), 0 , 32 )
background = pygame.image.load(bg).convert()
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == KEYDOWN:
if event.key == K_f:
win = pygame.display.set_mode((640 , 480 ), FULLSCREEN, 32 )
elif event.type == KEYUP:
if event.key ==K_f:
win = pygame.display.set_mode((640 , 480 ), 0 , 32 )
win.blit(background, (0 , 0 ))
pygame.display.update()
Other Events
並非所有events都需要被handle,例如在之前的例子中,使用
pygame.mouse.get_pos() 來取得游標位置,並不需要萃取MOUSEMOTION事件。此外,有時候我們需要屏蔽特定事件,讓其功能無法使用(例如尚未達到取得的條件),此時可以使用set_block()方法,e.g.
pygame.event.set_blocked(MOUSEMOTION)、pygame.event.set_blocked([KEYDOWN, KEYUP]) ,若是想要取消(unblock),傳入參數None即可(
pygame.event.set_blocked(MOUSEMOTION )。與set_block()相反的函數為
pygame.event.set_allowed() ,不過若是輸入參數為None,實際上為block all events。在使用之前,可以先使用
pygame.event.get_block() 方法來取得目前被屏蔽的事件。
看個例子:
import sys
import pygame
from pygame.locals import *
sp = 'sheep.png'
bg = 'background.png'
ScreenSize = (800 , 600 )
pygame.init()
win = pygame.display.set_mode(ScreenSize, RESIZABLE, 32 )
background = pygame.image.load(bg).convert()
font = pygame.font.SysFont("arial" , 80 )
text_surface = font.render("Game Message" , True , (0 , 0 , 255 ))
x = ScreenSize[0 ]
y = (ScreenSize[1 ] - text_surface.get_height())/2
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == MOUSEBUTTONDOWN:
if event.button == 3 :
pygame.event.set_blocked(MOUSEMOTION)
elif event.type == MOUSEBUTTONUP:
if event.button == 3 :
pygame.event.set_allowed(MOUSEMOTION)
if event.type == MOUSEMOTION:
win.blit(background, (0 , 0 ))
x -= 2
if x < -ScreenSize[0 ]:
x = ScreenSize[0 ]
win.blit(text_surface, (x, y))
pygame.display.update()
不處理MOUSEMOTION比較好,不然一開始若不移動滑鼠不會載入背景圖。
滑鼠按鍵的代號:left click(1),middle click(2),right click(3),scroll up(4),scroll down(5) 。
跑馬燈
上例為了測試set_blocked()函數才那樣設計,事實上可以寫成如下的自動跑馬燈形式。
import sys
import pygame
from pygame.locals import *
sp = 'sheep.png'
bg = 'background.png'
ScreenSize = (800 , 600 )
pygame.init()
win = pygame.display.set_mode(ScreenSize, RESIZABLE, 32 )
background = pygame.image.load(bg).convert()
font = pygame.font.SysFont("arial" , 80 )
text_surface = font.render("Game Message" , True , (0 , 0 , 255 ))
x = ScreenSize[0 ]
y = (ScreenSize[1 ] - text_surface.get_height())/2
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
win.blit(background, (0 , 0 ))
x -= 1
if x < -ScreenSize[0 ]:
x = ScreenSize[0 ]
win.blit(text_surface, (x, y))
pygame.display.update()
Draw
Pygame可以在視窗內繪製幾何圖形,首先先看怎麼設定pixels,可以在畫面上畫上小點。繪製時使用set_at()方法,可以輸入座標跟顏色。
<
from sys import exit
import pygame
from pygame.locals import *
import random as rd
ScreenSize = (800 , 600 )
def random_color () :
return rd.randint(0 , 255 ), rd.randint(0 , 255 ), rd.randint(0 , 255 )
def random_position () :
return rd.randint(0 , ScreenSize[0 ]), rd.randint(0 , ScreenSize[1 ])
pygame.init()
win = pygame.display.set_mode(ScreenSize, RESIZABLE, 32 )
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
if event.type == WINDOWSHOWN:
win.lock()
for _ in range(100 ):
rand_col = random_color()
rand_pos = random_position()
win.set_at(rand_pos, rand_col)
win.unlock()
pygame.display.update()
使用set_at(pos, color) 來繪製pixels。
當pygame繪製到一個suface時,必須先鎖定(locked),鎖定後pygame可以完全控制surface且電腦的其他程序都無法使用直至unlocked。這個步驟會自動執行,不過一直的lock跟unlock可能變得沒有效率。因此我們可以使用win.lock()方法先鎖定,等繪製完成後再解鎖(win.unlock()),如此可以稍微加快處理程序。
使用WINDOWSHOWN事件,可以控制在視窗一開始僅繪製一次,否則每個事件都會驅動一次繪製。
除了繪製pixels之外,pygame.draw可以用來繪製多種圖形。
Rectangle
from sys import exit
import pygame
from pygame.locals import *
import random as rd
ScreenSize = (800 , 600 )
def random_color () :
return rd.randint(0 , 255 ), rd.randint(0 , 255 ), rd.randint(0 , 255 )
def random_position () :
return rd.randint(0 , ScreenSize[0 ]), rd.randint(0 , ScreenSize[1 ])
def random_size (x, y) :
return rd.randint(5 , 20 ), rd.randint(5 , 20 )
pygame.init()
win = pygame.display.set_mode(ScreenSize)
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
if event.type == WINDOWSHOWN:
win.lock()
for _ in range(20 ):
color = random_color()
pos = random_position()
size = random_size(pos[0 ], pos[1 ])
line_width = rd.randint(0 , 5 )
pygame.draw.rect(win, color, pos+size, line_width)
win.unlock()
pygame.display.update()
使用pygame.draw.rect(win, color, pos+size, line_width)函數繪製矩形。
因為視窗圖片都是方形,所以pygame特別設計一個Rect物件 ,在繪製矩形的時候,第三個參數直接輸入Rect物件 即可。
最後一個參數是矩形外框線厚度,若值為0表示為實心矩形。
Polygon
from sys import exit
import pygame
from pygame.locals import *
import random as rd
ScreenSize = (800 , 600 )
def random_color () :
return rd.randint(0 , 255 ), rd.randint(0 , 255 ), rd.randint(0 , 255 )
def vertices () ->list:
"""
* 產生一個多邊形端點
"""
edges = rd.randint(3 , 10 )
points = []
for _ in range(edges):
x = rd.randint(0 , ScreenSize[0 ])
y = rd.randint(0 , ScreenSize[1 ])
points.append((x, y))
return points
pygame.init()
win = pygame.display.set_mode(ScreenSize)
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
if event.type == WINDOWSHOWN:
win.lock()
for _ in range(10 ):
color = random_color()
line_width = rd.randint(0 , 15 )
pygame.draw.polygon(win, color, vertices(), line_width)
win.unlock()
pygame.display.update()
跟矩形做法差不多,頂點的座標依順序置於list內做為第三個輸入參數即可。
Circle&Ellipse
from sys import exit
import pygame
from pygame.locals import *
import random as rd
ScreenSize = (800 , 600 )
def random_color () :
return rd.randint(0 , 255 ), rd.randint(0 , 255 ), rd.randint(0 , 255 )
def random_position () :
return rd.randint(0 , ScreenSize[0 ]), rd.randint(0 , ScreenSize[1 ])
pygame.init()
win = pygame.display.set_mode(ScreenSize)
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
if event.type == WINDOWSHOWN:
win.lock()
for _ in range(50 ):
color = random_color()
pos = random_position()
radius = rd.randint(5 , 20 )
line_width = rd.randint(0 , 15 )
pygame.draw.circle(win, color, pos, radius, line_width)
win.unlock()
pygame.display.update()
使用pygame.draw.circle(win, color, pos, radius, line_width)繪製圓形。
可以繪製橢圓形,同時亦可繪製圓形。
from sys import exit
import pygame
from pygame.locals import *
import random as rd
ScreenSize = (800 , 600 )
def random_color () :
return rd.randint(0 , 255 ), rd.randint(0 , 255 ), rd.randint(0 , 255 )
def random_position () :
return rd.randint(0 , ScreenSize[0 ]), rd.randint(0 , ScreenSize[1 ])
pygame.init()
win = pygame.display.set_mode(ScreenSize)
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
if event.type == WINDOWSHOWN:
win.lock()
for _ in range(50 ):
color = random_color()
pos = random_position()
axis = rd.randint(5 , 20 ), rd.randint(5 , 20 )
line_width = rd.randint(0 , 15 )
pygame.draw.ellipse(win, color, pos+axis, line_width)
win.unlock()
pygame.display.update()
Line&Arcs
from sys import exit
import pygame
from pygame.locals import *
import random as rd
ScreenSize = (800 , 600 )
def random_color () :
return rd.randint(0 , 255 ), rd.randint(0 , 255 ), rd.randint(0 , 255 )
pygame.init()
win = pygame.display.set_mode(ScreenSize)
ori_x, ori_y = None , None
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
if event.type == MOUSEBUTTONDOWN:
if event.button == 1 :
x, y = pygame.mouse.get_pos()
win.set_at((x, y), random_color())
if ori_x is None and ori_y is None :
ori_x = x
ori_y = y
else :
line_width = rd.randint(0 , 10 )
pygame.draw.line(win, random_color(), (ori_x, ori_y), (x,y), line_width)
ori_x = x
ori_y = y
if event.button == 3 :
ori_x = None
ori_y = None
pygame.display.update()
使用pygame.draw.line(win, random_color(), (ori_x, ori_y), (x,y))繪製直線,
試著將line替換為aaline(antialiasing)。
繪製arc時,使用pygame.draw.arc(win, random_color(), (x, y, width, height), startAngle, endAngle, line_width)方法,與線段類似,只是需要曲率。
from sys import exit
import pygame
from pygame.locals import *
import random as rd
import math
ScreenSize = (800 , 600 )
def random_color () :
return rd.randint(0 , 255 ), rd.randint(0 , 255 ), rd.randint(0 , 255 )
pygame.init()
win = pygame.display.set_mode(ScreenSize)
ori_x, ori_y = None , None
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
if event.type == MOUSEBUTTONDOWN:
if event.button == 1 :
x, y = pygame.mouse.get_pos()
win.set_at((x, y), random_color())
if ori_x is None and ori_y is None :
ori_x = x
ori_y = y
else :
line_width = rd.randint(0 , 10 )
startAngle = math.radians(rd.randint(0 , 360 ))
endAngle = math.radians(rd.randint(0 , 360 ))
pygame.draw.arc(win, random_color(), (ori_x, ori_y, abs(x-ori_x), abs(y-ori_y)), startAngle, endAngle, line_width)
ori_x = x
ori_y = y
if event.button == 3 :
ori_x = None
ori_y = None
pygame.display.update()
除了使用line繪製一條線,還可以使用pygame.draw.lines(win, random_color(), closed, points, line_width)來繪製折線,繪製時要給定一個list的點座標(points)。其中第三個參數closed是一個bool,若為True,表示最後會多劃一條頭尾相接的線來封閉折線區域,反之則否。
試試看將lines替換成aalines。
from sys import exit
import pygame
from pygame.locals import *
import random as rd
ScreenSize = (800 , 600 )
def random_color () :
return rd.randint(0 , 255 ), rd.randint(0 , 255 ), rd.randint(0 , 255 )
pygame.init()
win = pygame.display.set_mode(ScreenSize)
x, y = None , None
points = []
closed = True
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
if event.type == MOUSEBUTTONDOWN:
if event.button == 1 :
x, y = pygame.mouse.get_pos()
points.append((x,y))
win.set_at((x, y), random_color())
if event.button == 2 :
closed = not closed
if event.button == 3 :
line_width = rd.randint(0 , 10 )
pygame.draw.lines(win, random_color(), closed, points, line_width)
points = []
pygame.display.update()
Animation
遊戲難免要讓物件圖畫動起來,事實上在之前已經介紹過一些,e.g. 跑馬燈程式。現在先再試一個沿著一方向移動的圖片看看。
from sys import exit
import pygame
from pygame.locals import *
background = 'background.png'
sheep = 'sheep.png'
WinSize = (800 , 600 )
pygame.init()
win = pygame.display.set_mode(WinSize)
pygame.display.set_caption("Hello" )
bg = pygame.image.load(background).convert()
sp = pygame.image.load(sheep).convert_alpha()
x = 0
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
win.blit(bg, (0 , 0 ))
win.blit(sp, (x,400 ))
x += 1
if x > WinSize[0 ]:
x = 0
pygame.display.update()
Clock
上例中的做法每一次會移動1個pixel(x+=1),不過移動速度跟電腦效能有關,效能高的走得快,效能低的走得慢。我們希望移動速動是固定的,此時需要使用時間來控制速度,因此,在此我們可以使用Clock()物件。
from sys import exit
import pygame
from pygame.locals import *
background = 'background.png'
sheep = 'sheep.png'
WinSize = (800 , 600 )
pygame.init()
win = pygame.display.set_mode(WinSize)
pygame.display.set_caption("Hello" )
bg = pygame.image.load(background).convert()
sp = pygame.image.load(sheep).convert_alpha()
x, y = 0 , 0
clock = pygame.time.Clock()
speedX = 300
speedY = 200
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
win.blit(bg, (0 , 0 ))
timePass = clock.tick()
passSecondes = timePass/1000
movedX = passSecondes * speedX
movedY = passSecondes * speedY
x += movedX
y += movedY
if x > WinSize[0 ] - sp.get_width() or x < 0 :
speedX = -speedX
if y > WinSize[1 ] - sp.get_height() or y < 0 :
speedY = -speedY
win.blit(sp, (x, y))
pygame.display.update()
pygame.time.Clock()傳回一個Clock物件。使用Clock.tick()方法來更新時間,傳回值單位為milliseconds。這個方法應該被呼叫once per frame,也就是說我們得到的傳回值是上次呼叫到這次呼叫經過的時間,也就是每兩個frame間隔的時間。電影使用每秒24禎數(frame)就行了,電腦遊戲通常使用較多禎數(e.g. 30),超過70通常不太感覺得出來了。我們可以將禎數傳入到tick()來控制禎數,禎數越高原則上顯示越平順。
計算速度的時候使用的單位為pixels per second,所以我們先將經過的時間timePass換算成秒(除以1000),然後再乘以速度,可以得到移動的距離。如此不論電腦速度,移動的距離皆相同(因為是根據經過時間來計算)。
此處設計x向與y向有不同速度,當圖片碰觸到邊框,速度反向讓圖片反向移動。
自由落體
根據上例的技能,設計一個模擬自由落體的程式。
from sys import exit
import pygame
from pygame.locals import *
WinSize = (1024 , 1240 )
pygame.init()
win = pygame.display.set_mode(WinSize)
pygame.display.set_caption("Gravity" )
r = 20
x = WinSize[0 ]/2
h = 100
height = 100
black = (0 , 0 , 0 )
red = (255 , 0 , 0 )
clock = pygame.time.Clock()
t = 0
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
timePassed = clock.tick()
t += (timePassed / 1000 )
dh = 9.8 *t*t/2
height = h + dh
if WinSize[1 ] - height < r:
height = WinSize[1 ] - r
win.fill(black)
pygame.draw.circle(win, red, (x, height), r)
pygame.display.flip()
Collision Detection
要偵測兩個物品(或是游標與物品)是否接觸或碰撞(位置重疊),我們可以利用
pygame.Rect 物件來達成。先看一個簡單的例子:
from sys import exit
import pygame
from pygame.locals import *
WinSize = (800 , 600 )
pygame.init()
win = pygame.display.set_mode(WinSize)
pygame.display.set_caption("Collision Detection" )
black = (0 , 0 , 0 )
red = (255 , 0 , 0 )
blue = (0 , 0 , 255 )
color = red
r = 100
x = WinSize[0 ]/2
y = WinSize[1 ]/2
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
win.fill(black)
circle = pygame.draw.circle(win, color, (x, y), r)
cursor = pygame.mouse.get_pos()
collision = circle.collidepoint(cursor)
color = blue if collision else red
pygame.display.update()
有了點跟物件的偵測,接著嘗試物件跟物件的碰撞偵測。
from sys import exit
import pygame
from pygame.locals import *
WinSize = (800 , 600 )
pygame.init()
win = pygame.display.set_mode(WinSize)
pygame.display.set_caption("Collision Detection" )
black = (0 , 0 , 0 )
red = (255 , 0 , 0 )
blue = (0 , 0 , 255 )
green = (0 , 255 , 0 )
grey = (100 , 100 , 100 )
color = red
center = (WinSize[0 ]/2 , WinSize[1 ]/2 )
r = 100
ballCenter = (100 , 100 )
ballColor = green
ballRadius = 20
drag = False
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
if event.type == MOUSEBUTTONDOWN:
if event.button == 1 :
if green_ball.collidepoint(event.pos):
drag = True
if event.type == MOUSEBUTTONUP:
if event.button == 1 :
drag = False
if event.type == MOUSEMOTION:
if drag:
ballCenter = event.pos
win.fill(black)
circle = pygame.draw.circle(win, color, center, r)
green_ball = pygame.draw.circle(win, ballColor, ballCenter, ballRadius)
collision = circle.colliderect(green_ball)
color = blue if collision else red
pygame.display.update()
設計兩個圓並使用colliderect()來偵測其是否有collision。
這裡使用拖曳(drag),先判斷MOUSEBUTTONDOWN,如果游標在球上方,開始drag。如果放開按鍵,結束drag。drag期間,球中心座標為游標座標。
兩圖碰撞偵測
修改之前的單圖動畫,使其成為兩圖動畫並偵測碰撞,碰撞時反向移動。
from sys import exit
import pygame
from pygame.locals import *
background = 'background.png'
sheep = 'sheep.png'
WinSize = (800 , 600 )
pygame.init()
win = pygame.display.set_mode(WinSize)
pygame.display.set_caption("Collision Detection" )
bg = pygame.image.load(background).convert()
sp = pygame.image.load(sheep).convert_alpha()
sp2 = pygame.image.load(sheep).convert_alpha()
x1, y1 = 0 , 0
x2, y2 = 0 , WinSize[1 ] - sp2.get_height()
clock = pygame.time.Clock()
speedX = 300
speedY = 200
speedX2 = 250
speedY2 = 350
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
win.blit(bg, (0 , 0 ))
timePass = clock.tick()
passSecondes = timePass/1000
movedX = passSecondes * speedX
movedY = passSecondes * speedY
x1 += movedX
y1 += movedY
if x1 > WinSize[0 ] - sp.get_width() or x1 < 0 :
speedX = -speedX
if y1 > WinSize[1 ] - sp.get_height() or y1 < 0 :
speedY = -speedY
movedX2 = passSecondes * speedX2
movedY2 = passSecondes * speedY2
x2 += movedX2
y2 += movedY2
if x2 > WinSize[0 ] - sp2.get_width() or x2 < 0 :
speedX2 = -speedX2
if y2 > WinSize[1 ] - sp2.get_height() or y2 < 0 :
speedY2 = -speedY2
spRect = sp.get_rect(x=x1, y=y1)
sp2Rect = sp2.get_rect(x=x2, y=y2)
if spRect.colliderect(sp2Rect):
speedX = -speedX
speedY = -speedY
speedX2 = -speedX2
speedY2 = -speedY2
win.blit(sp, (x1, y1))
win.blit(sp2, (x2, y2))
pygame.display.update()
多圖碰撞偵測
測試多個移動圖形相互碰撞的情形。
from sys import exit
import pygame
from pygame.locals import *
import random as rd
background = 'background.png'
sheep = 'sheep.png'
WinSize = (800 , 600 )
pygame.init()
win = pygame.display.set_mode(WinSize)
pygame.display.set_caption("Collision Detection" )
bg = pygame.image.load(background).convert()
class Sheep (object) :
def __init__ (self, x=0 , y=0 , speed_x=0 , speed_y=0 ) :
self.x = x
self.y = y
self.speed_x = speed_x
self.speed_y = speed_y
self.image = pygame.image.load(sheep).convert_alpha()
def new_pos (self, t) :
moved_x = t * self.speed_x
moved_y = t * self.speed_y
self.x += moved_x
self.y += moved_y
if self.x >= WinSize[0 ] - self.image.get_width():
self.x = WinSize[0 ] - self.image.get_width()
self.speed_x = -self.speed_x
if self.x <= 0 :
self.x = 0
self.speed_x = -self.speed_x
if self.y >= WinSize[1 ] - self.image.get_height():
self.y = WinSize[1 ] - self.image.get_height()
self.speed_y = -self.speed_y
if self.y <= 0 :
self.y = 0
self.speed_y = -self.speed_y
def rect (self) :
"""
return: image的Rect物件
"""
return self.image.get_rect(x=self.x, y=self.y)
sp1 = Sheep(0 , 0 , 300 , 200 )
sp2 = Sheep(0 , WinSize[1 ]-sp1.image.get_height(), 250 , 350 )
sp3 = Sheep(WinSize[0 ]-sp1.image.get_width(), 0 , 300 , 300 )
sp4 = Sheep(WinSize[0 ]-sp1.image.get_width(), WinSize[1 ]-sp1.image.get_height(), 350 , 250 )
sp5 = Sheep(WinSize[0 ]/2 , WinSize[1 ]/2 , -125 , 175 )
sp6 = Sheep(50 , 50 , 500 , 500 )
sp7 = Sheep(50 , 50 , 400 , 700 )
flock = [sp1, sp2, sp3, sp4, sp5]
clock = pygame.time.Clock()
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
win.blit(bg, (0 , 0 ))
time_passed = clock.tick()
passSecondes = time_passed/1000
for s in flock:
s.new_pos(passSecondes)
for i, s in enumerate(flock):
for j, r in enumerate(flock):
if i==j:
continue
else :
if s.rect().colliderect(r.rect()):
s.x -= passSecondes * s.speed_x
s.y -= passSecondes * s.speed_y
s.speed_x = -s.speed_x
s.speed_y = -s.speed_y
for s in flock:
win.blit(s.image, (s.x, s.y))
pygame.display.update()
因為有多個物件,建立class整理一下。
在碰撞時,將物件位置回復至碰撞前,跑起來比較順,因為若沒這步驟,可能兩個物件已然重疊。增加sp6跟sp7測試,若是一開始即重疊很多,最後會卡在一起。
碰撞後可以增加一點方向跟速度的變化。
此處的碰撞依然採用colliderect()來偵測倆倆之間是否有碰撞。以下的例子使用collidelist()來偵測某rect與一個list內的所有rect是否有碰撞(僅修改上例中偵測兩圖碰撞的部分)。
from sys import exit
import pygame
from pygame.locals import *
import random as rd
background = 'background.png'
sheep = 'sheep.png'
WinSize = (800 , 600 )
pygame.init()
win = pygame.display.set_mode(WinSize)
pygame.display.set_caption("Collision Detection" )
bg = pygame.image.load(background).convert()
class Sheep (object) :
def __init__ (self, x=0 , y=0 , speed_x=0 , speed_y=0 ) :
self.x = x
self.y = y
self.speed_x = speed_x
self.speed_y = speed_y
self.image = pygame.image.load(sheep).convert_alpha()
def new_pos (self, t) :
moved_x = t * self.speed_x
moved_y = t * self.speed_y
self.x += moved_x
self.y += moved_y
if self.x >= WinSize[0 ] - self.image.get_width():
self.x = WinSize[0 ] - self.image.get_width()
self.speed_x = -self.speed_x
if self.x <= 0 :
self.x = 0
self.speed_x = -self.speed_x
if self.y >= WinSize[1 ] - self.image.get_height():
self.y = WinSize[1 ] - self.image.get_height()
self.speed_y = -self.speed_y
if self.y <= 0 :
self.y = 0
self.speed_y = -self.speed_y
def rect (self) :
"""
return: image的Rect物件
"""
return self.image.get_rect(x=self.x, y=self.y)
sp1 = Sheep(0 , 0 , 300 , 200 )
sp2 = Sheep(0 , WinSize[1 ]-sp1.image.get_height(), 250 , 350 )
sp3 = Sheep(WinSize[0 ]-sp1.image.get_width(), 0 , 300 , 300 )
sp4 = Sheep(WinSize[0 ]-sp1.image.get_width(), WinSize[1 ]-sp1.image.get_height(), 350 , 250 )
sp5 = Sheep(WinSize[0 ]/2 , WinSize[1 ]/2 , -125 , 175 )
sp6 = Sheep(50 , 50 , 500 , 500 )
sp7 = Sheep(50 , 50 , 400 , 700 )
flock = [sp1, sp2, sp3, sp4, sp5]
clock = pygame.time.Clock()
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
win.blit(bg, (0 , 0 ))
time_passed = clock.tick()
passSecondes = time_passed/1000
for s in flock:
s.new_pos(passSecondes)
for i, s in enumerate(flock):
others = [f.rect() for f in flock if s != f]
if s.rect().collidelist(others) >= 0 :
s.x -= passSecondes * s.speed_x
s.y -= passSecondes * s.speed_y
s.speed_x = -s.speed_x
s.speed_y = -s.speed_y
for s in flock:
win.blit(s.image, (s.x, s.y))
pygame.display.update()
Exercise: 練習多個幾何圖形碰撞的偵測。
Sprits
在Pygame中,事實上已經有針對一個遊戲元件所定義的物件,稱為Sprite。我們可以直接繼承該物件來建立自己的Sprite,而且Sprite也內建碰撞偵測可供使用。現在將上例之多圖碰撞修改如下:
from sys import exit
import pygame
from pygame.locals import *
import random as rd
background = 'background.png'
sheep = 'sheep.png'
WinSize = (800 , 600 )
pygame.init()
win = pygame.display.set_mode(WinSize)
pygame.display.set_caption("Collision Detection" )
bg = pygame.image.load(background).convert()
class Sheep (pygame.sprite.Sprite) :
def __init__ (self, sid, x=0 , y=0 , speed_x=0 , speed_y=0 ) :
super().__init__()
self.sid = sid
self.x = x
self.y = y
self.speed_x = speed_x
self.speed_y = speed_y
self.image = pygame.image.load(sheep).convert()
self.rect = self.image.get_rect(x=self.x, y=self.y)
self.mask = pygame.mask.from_surface(self.image)
def update (self, t) :
moved_x = t * self.speed_x
moved_y = t * self.speed_y
self.x += moved_x
self.y += moved_y
self.rect.center = (self.x, self.y)
if self.x >= WinSize[0 ] - self.image.get_width():
self.x = WinSize[0 ] - self.image.get_width()
self.speed_x = -self.speed_x
if self.x <= 0 :
self.x = 0
self.speed_x = -self.speed_x
if self.y >= WinSize[1 ] - self.image.get_height():
self.y = WinSize[1 ] - self.image.get_height()
self.speed_y = -self.speed_y
if self.y <= 0 :
self.y = 0
self.speed_y = -self.speed_y
sp1 = Sheep(1 , 0 , 0 , 300 , 200 )
sp2 = Sheep(2 , 0 , WinSize[1 ]-sp1.image.get_height(), 250 , 350 )
sp3 = Sheep(3 , WinSize[0 ]-sp1.image.get_width(), 0 , 300 , 300 )
sp4 = Sheep(4 , WinSize[0 ]-sp1.image.get_width(), WinSize[1 ]-sp1.image.get_height(), 350 , 250 )
sp5 = Sheep(5 , WinSize[0 ]/2 , WinSize[1 ]/2 , -125 , 175 )
sp6 = Sheep(6 , 50 , 50 , 500 , 500 )
sp7 = Sheep(7 , 50 , 50 , 400 , 700 )
flock = [sp1, sp2, sp3, sp4, sp5]
clock = pygame.time.Clock()
while True :
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
win.blit(bg, (0 , 0 ))
time_passed = clock.tick()
passSecondes = time_passed/1000
for s in flock:
s.update(passSecondes)
for i, s in enumerate(flock):
others = pygame.sprite.Group()
for j, o in enumerate(flock):
if i != j:
others.add(o)
ss = pygame.sprite.spritecollide(s, others, False , pygame.sprite.collide_mask)
if ss:
for z in ss:
print(s.sid, z.sid)
print("-------------" )
s.x -= passSecondes * s.speed_x
s.y -= passSecondes * s.speed_y
s.speed_x = -s.speed_x
s.speed_y = -s.speed_y
for s in flock:
win.blit(s.image, (s.x, s.y))
pygame.display.update()
Kokiko
設計一個kokiko遊戲。
from sys import exit
import pygame
from pygame.locals import *
import random as rd
sheep = 'sheep.png'
silver = (192 , 192 , 192 )
black = (0 , 0 , 0 )
red = (255 , 0 , 0 )
brick_red = (183 , 50 , 57 )
green = (0 , 255 , 0 )
blue = (0 , 0 , 255 )
white = (255 , 255 , 255 )
W = 1000
H = 800
BallW = 10
BallH = 10
BrickW = 50
BrickH = 30
ScoreArea = 80
Lives = 2
speed_increment = 10
pygame.init()
win = pygame.display.set_mode((W,H))
pygame.display.set_caption("Kokiko" )
sp = pygame.image.load(sheep).convert()
class Ball (pygame.sprite.Sprite) :
score = 0
def __init__ (self, ball_id=0 , ball_x=0 , ball_y=0 , speed_x=0 , speed_y=0 ) :
super().__init__()
self.ball_id = ball_id
self.ball_x = ball_x
self.ball_y = ball_y
self.speed_x = speed_x
self.speed_y = speed_y
self.image = pygame.Surface((BallW, BallH))
self.image.fill(silver)
self.rect = self.image.get_rect(x=self.ball_x, y=self.ball_y)
self.mask = pygame.mask.from_surface(self.image)
self.running = True
def draw (self, screen) :
screen.blit(self.image, (self.ball_x, self.ball_y))
def update (self, t) :
moved_x = t * self.speed_x
moved_y = t * self.speed_y
self.ball_x += moved_x
self.ball_y += moved_y
self.rect.center = (self.ball_x, self.ball_y)
if self.ball_x >= W - BallW:
self.ball_x = W - BallW
self.speed_x = -self.speed_x
if self.ball_x <= 0 :
self.ball_x = 0
self.speed_x = -self.speed_x
if self.ball_y >= H - BallH:
self.ball_y = H - BallH
self.speed_y = -self.speed_y
if self.ball_y < 0 :
self.running = False
class Brick (pygame.sprite.Sprite) :
def __init__ (self, x, y) :
super().__init__()
self.x = x - BrickW/2
self.y = y - BrickH/2
self.image = pygame.Surface((BrickW, BrickH))
self.image.fill(brick_red)
self.rect = self.image.get_rect(x=self.x, y=self.y)
self.mask = pygame.mask.from_surface(self.image)
def draw (self, screen) :
self.image.fill(brick_red)
screen.blit(self.image, (self.x , self.y))
class CursorBrick (pygame.sprite.Sprite) :
def __init__ (self) :
super().__init__()
self.image = pygame.Surface((BrickW, BrickH))
self.image.fill(green)
self.rect = self.image.get_rect()
self.mask = pygame.mask.from_surface(self.image)
def draw (self, screen) :
screen.blit(self.image, (self.rect.x, self.rect.y))
def update (self) :
mx, my = pygame.mouse.get_pos()
self.rect.x = mx-BrickW/2
self.rect.y = my-BrickH/2
if my <= ScoreArea:
self.image.fill(blue)
ball = Ball(ball_x=rd.randint(1 , W-BallW-1 ), ball_y=H-BallW-1 , speed_x=300 , speed_y=-200 )
brick_group = pygame.sprite.Group()
cursor_brick = CursorBrick()
score_font = pygame.font.SysFont(None , 60 )
clock = pygame.time.Clock()
while True :
win.fill(black)
game_score = score_font.render(f"Score: {Ball.score:.0f}" , True , white)
game_lives = score_font.render(f"Lives: {Lives}" , True , white)
win.blit(game_score, (10 , 10 ))
win.blit(game_lives, (W-10 -game_lives.get_width(), 10 ))
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
if event.type == MOUSEBUTTONDOWN:
if event.button == 1 and ball.running:
x, y = pygame.mouse.get_pos()
if not ss and y > ScoreArea:
brick = Brick(x, y)
brick_group.add(brick)
if event.button == 3 :
if not ball.running and Lives > 0 :
ball = Ball(ball_x=rd.randint(1 , W-BallW-1 ), ball_y=H-BallW-1 , speed_x=300 , speed_y=-200 )
Lives -= 1
if event.type == KEYDOWN:
if event.key == K_SPACE and ball.running:
x, y = pygame.mouse.get_pos()
if not ss and y > ScoreArea:
brick = Brick(x, y)
brick_group.add(brick)
if event.type == WINDOWSHOWN:
pygame.draw.rect(win, white, Rect((100 , 100 ), (100 , 100 )), 1 )
time_passed = clock.tick()
pass_seconds = time_passed / 1000
if ball.running:
Ball.score += pass_seconds
if Ball.score > 100 :
inc = Ball.score // 100
if ball.speed_x > 0 :
ball.speed_x = 300 + inc * speed_increment
else :
ball.speed_x = -300 - inc * speed_increment
if ball.speed_y > 0 :
ball.speed_y = 200 + inc * speed_increment
else :
ball.speed_y = -200 - inc * speed_increment
ball.update(pass_seconds)
ball.draw(win)
cursor_brick.update()
cursor_brick.draw(win)
for brick in brick_group:
brick.update()
collisions = pygame.sprite.spritecollide(ball, brick_group, True , pygame.sprite.collide_mask)
if collisions:
ball_left = ball.ball_x - ball.speed_x * pass_seconds
ball_top = ball.ball_y - ball.speed_y * pass_seconds
brick_left = collisions[0 ].x - BrickW / 2
brick_top = collisions[0 ].y - BrickH / 2
ball_right = ball_left + BallW
ball_bottom = ball_top + BallH
brick_right = brick_left + BrickW
brick_bottom = brick_top + BrickH
speed_slope = abs(ball.speed_y)/abs(ball.speed_x)
if ball.speed_x > 0 and ball.speed_y > 0 :
if brick_top <= ball_bottom and brick_left >= ball_right:
ball.speed_x = -ball.speed_x
elif brick_top >= ball_bottom and brick_left <= ball_right:
ball.speed_y = -ball.speed_y
else :
if (ball_bottom-brick_top)/(ball_right-brick_left) <= speed_slope:
ball.speed_x = -ball.speed_x
else :
ball.speed_y = -ball.speed_y
if ball.speed_x > 0 and ball.speed_y < 0 :
if brick_bottom < ball_top:
if ball_right < brick_left:
if abs(ball_top - brick_bottom) / abs(ball_right - brick_left) < speed_slope:
ball.speed_x = -ball.speed_x
else :
ball.speed_y = -ball.speed_y
else :
ball.speed_y = -ball.speed_y
else :
ball.speed_x = -ball.speed_x
if ball.speed_x < 0 and ball.speed_y > 0 :
if brick_top > ball_bottom:
if ball_left < brick_right:
if abs(ball_bottom - brick_top) / abs(ball_left - brick_right) < speed_slope:
ball.speed_x = -ball.speed_x
else :
ball.speed_y = -ball.speed_y
else :
ball.speed_y = -ball.speed_y
else :
ball.speed_x = -ball.speed_x
if ball.speed_x < 0 and ball.speed_y < 0 :
if brick_bottom < ball_top:
if ball_left < brick_right:
if abs(ball_top - brick_bottom) / abs(ball_left - brick_right) < speed_slope:
ball.speed_x = -ball.speed_x
else :
ball.speed_y = -ball.speed_y
else :
ball.speed_y = -ball.speed_y
else :
ball.speed_x = -ball.speed_x
Ball.score += 10
ss = pygame.sprite.spritecollide(cursor_brick, brick_group, False , pygame.sprite.collide_mask)
if ss:
cursor_brick.image.fill(blue)
for s in ss:
s.image.fill(white)
else :
cursor_brick.image.fill(green)
if not ss:
for brick in brick_group.sprites():
brick.image.fill(brick_red)
brick_group.draw(win)
pygame.display.update()
使用sprite物件,並使用spritecollide()來判斷碰撞。
球若跑出上方邊界算失敗,點擊(或按空白鍵)來放置磚塊。磚塊不能相互重疊。失敗後若還有lives,點擊右鍵重新開始。
試了多個方法來判斷碰撞的方向(亦即是磚塊的哪一面受到撞擊),可惜效果並不很好,有些情況下還是沒有反轉。
-->