跳到主要内容Python 图形界面与游戏开发实战:计算器、记事本及经典小游戏 | 极客日志Python算法
Python 图形界面与游戏开发实战:计算器、记事本及经典小游戏
综述由AI生成使用 Python 进行图形界面和游戏开发的六个实战项目,包括基于 Tkinter 的计算器、记事本、用户登录注册模块,以及基于 Pygame 的贪吃蛇、俄罗斯方块和连连看游戏。内容详细阐述了各项目的实现原理、核心逻辑与完整代码,重点讲解了界面布局、事件处理、文件序列化存储及游戏状态机设计,适合具备 Python 基础的学习者参考实践。
DotNetGuy17 浏览 Python 图形界面与游戏开发实战
本文介绍六个基于 Python 的实用项目,涵盖 Tkinter 图形界面编程与 Pygame 游戏开发。内容包括简易计算器、文本编辑器、用户登录注册模块,以及贪吃蛇、俄罗斯方块和连连看等经典游戏的实现原理与核心代码。
1. 图形化计算器
案例介绍
本例利用 Python 开发一个可以进行简单四则运算的图形化计算器,使用 Tkinter 图形组件进行开发。主要知识点包括 Python Tkinter 界面编程和计算器逻辑运算实现。难度为初级,适合具有 Python 基础和 Tkinter 组件编程知识的用户学习。
设计原理
要制作一个计算器,首先需要知道它由哪些部分组成。从结构上来说,一个简单的图形界面需要由界面组件、组件的事件监听器(响应各类事件的逻辑)和具体的事件处理逻辑组成。界面实现的主要工作是创建各个界面组件对象,对其进行初始化,以及控制各组件之间的层次关系和布局。
示例源码
import tkinter
import math
import tkinter.messagebox
class Calculator(object):
def __init__(self):
self.root = tkinter.Tk()
self.root.minsize(280, 450)
self.root.maxsize(280, 470)
self.root.title('计算器')
self.result = tkinter.StringVar()
self.result.set(0)
self.lists = []
self.ispresssign = False
self.menus()
self.layout()
self.root.mainloop()
def menus(self):
allmenu = tkinter.Menu(self.root)
filemenu = tkinter.Menu(allmenu, tearoff=0)
filemenu.add_command(label='标准型', command=self.myfunc)
filemenu.add_separator()
filemenu.add_command(label=, command=.myfunc)
allmenu.add_cascade(label=, menu=filemenu)
editmenu = tkinter.Menu(allmenu, tearoff=)
editmenu.add_command(label=, command=.myfunc)
editmenu.add_command(label=, command=.myfunc)
allmenu.add_cascade(label=, menu=editmenu)
helpmenu = tkinter.Menu(allmenu, tearoff=)
helpmenu.add_command(label=, command=.myfunc)
allmenu.add_cascade(label=, menu=helpmenu)
.root.config(menu=allmenu)
():
show_label = tkinter.Label(.root, bd=, bg=, font=(, ), anchor=, textvariable=.result)
show_label.place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=.wait).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=.wait).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=.wait).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=.wait).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=.wait).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=.dele_one).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .result.()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=.sweeppress).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=.zf).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=.kpf).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .pressnum()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .pressnum()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .pressnum()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .presscalculate()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .presscalculate()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .pressnum()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .pressnum()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .pressnum()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .presscalculate()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=.ds).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .pressnum()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .pressnum()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .pressnum()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .presscalculate()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .pressequal()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .pressnum()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .pressnum()).place(x=, y=, width=, height=)
tkinter.Button(.root, text=, command=: .presscalculate()).place(x=, y=, width=, height=)
():
tkinter.messagebox.showinfo(, )
():
.ispresssign == :
:
.result.()
.ispresssign =
num == :
num =
oldnum = .result.get()
oldnum == :
.result.(num)
:
newnum = oldnum + num
.result.(newnum)
():
num = .result.get()
.lists.append(num)
.lists.append(sign)
.ispresssign =
():
curnum = .result.get()
.lists.append(curnum)
calculatestr = .join(.lists)
endnum = (calculatestr)
.result.((endnum)[:])
.lists != :
.ispresssign =
.lists.clear()
():
tkinter.messagebox.showinfo(, )
():
.result.get() == .result.get() == :
.result.()
:
num = (.result.get())
num > :
strnum = .result.get()
strnum = strnum[:num - ]
.result.(strnum)
:
.result.()
():
strnum = .result.get()
strnum[] == :
.result.(strnum[:])
strnum[] != strnum != :
.result.( + strnum)
():
dsnum = / (.result.get())
.result.((dsnum)[:])
.lists != :
.ispresssign =
.lists.clear()
():
.lists.clear()
.result.()
():
strnum = (.result.get())
endnum = math.sqrt(strnum)
(endnum)[-] == :
.result.((endnum)[:-])
:
.result.((endnum)[:])
.lists != :
.ispresssign =
.lists.clear()
__name__ == :
my_calculator = Calculator()
'关于计算器'
self
'查看'
0
'复制'
self
'粘贴'
self
'编辑'
0
'帮助'
self
'帮助'
self
def
layout
self
self
3
'white'
'宋体'
30
'e'
self
5
20
270
70
self
'MC'
self
5
95
50
50
self
'MR'
self
60
95
50
50
self
'MS'
self
115
95
50
50
self
'M+'
self
170
95
50
50
self
'M-'
self
225
95
50
50
self
'←'
self
5
150
50
50
self
'CE'
lambda
self
set
0
60
150
50
50
self
'C'
self
115
150
50
50
self
'±'
self
170
150
50
50
self
'√'
self
225
150
50
50
self
'7'
lambda
self
'7'
5
205
50
50
self
'8'
lambda
self
'8'
60
205
50
50
self
'9'
lambda
self
'9'
115
205
50
50
self
'/'
lambda
self
'/'
170
205
50
50
self
'//'
lambda
self
'//'
225
205
50
50
self
'4'
lambda
self
'4'
5
260
50
50
self
'5'
lambda
self
'5'
60
260
50
50
self
'6'
lambda
self
'6'
115
260
50
50
self
'*'
lambda
self
'*'
170
260
50
50
self
'1/x'
self
225
260
50
50
self
'1'
lambda
self
'1'
5
315
50
50
self
'2'
lambda
self
'2'
60
315
50
50
self
'3'
lambda
self
'3'
115
315
50
50
self
'-'
lambda
self
'-'
170
315
50
50
self
'='
lambda
self
225
315
50
105
self
'0'
lambda
self
'0'
5
370
105
50
self
'.'
lambda
self
'.'
115
370
50
50
self
'+'
lambda
self
'+'
170
370
50
50
def
myfunc
self
''
'预留接口'
def
pressnum
self, num
if
self
False
pass
else
self
set
0
self
False
if
'.'
'0.'
self
if
'0'
self
set
else
self
set
def
presscalculate
self, sign
self
self
self
self
True
def
pressequal
self
self
self
''
self
eval
self
set
str
10
if
self
0
self
True
self
def
wait
self
''
'更新中...'
def
dele_one
self
if
self
''
or
self
'0'
self
set
'0'
return
else
len
self
if
1
self
0
1
self
set
else
self
set
'0'
def
zf
self
self
if
0
'-'
self
set
1
elif
0
'-'
and
'0'
self
set
'-'
def
ds
self
1
int
self
self
set
str
10
if
self
0
self
True
self
def
sweeppress
self
self
self
set
0
def
kpf
self
float
self
if
str
1
'0'
self
set
str
2
else
self
set
str
10
if
self
0
self
True
self
if
'__main__'
2. 文本编辑器
案例介绍
tkinter 是 Python 下面向 tk 的图形界面接口库,可以方便地进行图形界面设计和交互操作编程。本例采用 Python 3.8 版本,实现了新建、打开、保存、另存为、查找等基本功能。
示例源码
from tkinter import *
from tkinter.filedialog import *
from tkinter.messagebox import *
import os
filename = ""
def author():
showinfo(title="作者", message="Python")
def power():
showinfo(title="版权信息", message="课堂练习")
def mynew():
global top, filename, textPad
top.title("未命名文件")
filename = None
textPad.delete(1.0, END)
def myopen():
global filename
filename = askopenfilename(defaultextension=".txt")
if filename == "":
filename = None
else:
top.title("记事本" + os.path.basename(filename))
textPad.delete(1.0, END)
f = open(filename, 'r')
textPad.insert(1.0, f.read())
f.close()
def mysave():
global filename
try:
f = open(filename, 'w')
msg = textPad.get(1.0, 'end')
f.write(msg)
f.close()
except:
mysaveas()
def mysaveas():
global filename
f = asksaveasfilename(initialfile="未命名.txt", defaultextension=".txt")
filename = f
fh = open(f, 'w')
msg = textPad.get(1.0, END)
fh.write(msg)
fh.close()
top.title("记事本 " + os.path.basename(f))
def cut():
global textPad
textPad.event_generate("<<Cut>>")
def copy():
global textPad
textPad.event_generate("<<Copy>>")
def paste():
global textPad
textPad.event_generate("<<Paste>>")
def undo():
global textPad
textPad.event_generate("<<Undo>>")
def redo():
global textPad
textPad.event_generate("<<Redo>>")
def select_all():
global textPad
textPad.tag_add("sel", "1.0", "end")
def find():
t = Toplevel(top)
t.title("查找")
t.geometry("260x60+200+250")
t.transient(top)
Label(t, text="查找:").grid(row=0, column=0, sticky="e")
v = StringVar()
e = Entry(t, width=20, textvariable=v)
e.grid(row=0, column=1, padx=2, pady=2, sticky="we")
e.focus_set()
c = IntVar()
Checkbutton(t, text="不区分大小写", variable=c).grid(row=1, column=1, sticky='e')
Button(t, text="查找所有", command=lambda: search(v.get(), c.get(), textPad, t, e)).grid(row=0, column=2, sticky="e" + "w", padx=2, pady=2)
def close_search():
textPad.tag_remove("match", "1.0", END)
t.destroy()
t.protocol("WM_DELETE_WINDOW", close_search)
def mypopup(event):
editmenu.tk_popup(event.x_root, event.y_root)
def search(needle, cssnstv, textPad, t, e):
textPad.tag_remove("match", "1.0", END)
count = 0
if needle:
pos = "1.0"
while True:
pos = textPad.search(needle, pos, nocase=cssnstv, stopindex=END)
if not pos:
break
lastpos = pos + str(len(needle))
textPad.tag_add("match", pos, lastpos)
count += 1
pos = lastpos
textPad.tag_config('match', fg='yellow', bg="green")
e.focus_set()
t.title(str(count) + "个被匹配")
top = Tk()
top.title("记事本")
top.geometry("600x400+100+50")
menubar = Menu(top)
filemenu = Menu(top)
filemenu.add_command(label="新建", accelerator="Ctrl+N", command=mynew)
filemenu.add_command(label="打开", accelerator="Ctrl+O", command=myopen)
filemenu.add_command(label="保存", accelerator="Ctrl+S", command=mysave)
filemenu.add_command(label="另存为", accelerator="Ctrl+shift+s", command=mysaveas)
menubar.add_cascade(label="文件", menu=filemenu)
editmenu = Menu(top)
editmenu.add_command(label="撤销", accelerator="Ctrl+Z", command=undo)
editmenu.add_command(label="重做", accelerator="Ctrl+Y", command=redo)
editmenu.add_separator()
editmenu.add_command(label="剪切", accelerator="Ctrl+X", command=cut)
editmenu.add_command(label="复制", accelerator="Ctrl+C", command=copy)
editmenu.add_command(label="粘贴", accelerator="Ctrl+V", command=paste)
editmenu.add_separator()
editmenu.add_command(label="查找", accelerator="Ctrl+F", command=find)
editmenu.add_command(label="全选", accelerator="Ctrl+A", command=select_all)
menubar.add_cascade(label="编辑", menu=editmenu)
aboutmenu = Menu(top)
aboutmenu.add_command(label="作者", command=author)
aboutmenu.add_command(label="版权", command=power)
menubar.add_cascade(label="关于", menu=aboutmenu)
top['menu'] = menubar
textPad = Text(top, undo=True)
textPad.pack(expand=YES, fill=BOTH)
scroll = Scrollbar(textPad)
textPad.config(yscrollcommand=scroll.set)
scroll.config(command=textPad.yview)
scroll.pack(side=RIGHT, fill=Y)
textPad.bind("<Control-N>", mynew)
textPad.bind("<Control-n>", mynew)
textPad.bind("<Control-O>", myopen)
textPad.bind("<Control-o>", myopen)
textPad.bind("<Control-S>", mysave)
textPad.bind("<Control-s>", mysave)
textPad.bind("<Control-A>", select_all)
textPad.bind("<Control-a>", select_all)
textPad.bind("<Control-F>", find)
textPad.bind("<Control-f>", find)
textPad.bind("<Button-3>", mypopup)
top.mainloop()
3. 用户登录与注册
案例介绍
本例设计一个用户登录和注册模块,使用 Tkinter 框架构建界面,主要用到画布、文本框、按钮等组件。涉及知识点包括 Python Tkinter 界面编程和 pickle 数据存储。通过 pickle 模块的序列化操作能够将程序中运行的对象信息保存到文件中去,永久存储;通过反序列化操作,能够从文件中创建上一次程序保存的对象。
示例源码
import tkinter as tk
from tkinter import messagebox
import pickle
import os
class LoginApp:
def __init__(self, root):
self.root = root
self.root.title("用户登录系统")
self.data_file = "users.pkl"
self.users = self.load_users()
tk.Label(root, text="用户名:").pack(pady=5)
self.username_entry = tk.Entry(root)
self.username_entry.pack(pady=5)
tk.Label(root, text="密码:").pack(pady=5)
self.password_entry = tk.Entry(root, show="*")
self.password_entry.pack(pady=5)
btn_frame = tk.Frame(root)
btn_frame.pack(pady=10)
tk.Button(btn_frame, text="登录", command=self.login).pack(side=tk.LEFT, padx=5)
tk.Button(btn_frame, text="注册", command=self.register).pack(side=tk.LEFT, padx=5)
tk.Button(btn_frame, text="退出", command=root.quit).pack(side=tk.LEFT, padx=5)
def load_users(self):
if os.path.exists(self.data_file):
with open(self.data_file, 'rb') as f:
return pickle.load(f)
return {}
def save_users(self):
with open(self.data_file, 'wb') as f:
pickle.dump(self.users, f)
def register(self):
user = self.username_entry.get().strip()
pwd = self.password_entry.get().strip()
if not user or not pwd:
messagebox.showwarning("提示", "用户名和密码不能为空")
return
if user in self.users:
messagebox.showerror("错误", "用户名已存在")
else:
self.users[user] = pwd
self.save_users()
messagebox.showinfo("成功", "注册成功")
self.username_entry.delete(0, tk.END)
self.password_entry.delete(0, tk.END)
def login(self):
user = self.username_entry.get().strip()
pwd = self.password_entry.get().strip()
if user in self.users and self.users[user] == pwd:
messagebox.showinfo("成功", "登录成功")
else:
messagebox.showerror("错误", "用户名或密码错误")
if __name__ == '__main__':
root = tk.Tk()
app = LoginApp(root)
root.mainloop()
4. 贪吃蛇游戏
案例介绍
贪吃蛇是一款经典的益智游戏。该游戏通过控制蛇头方向吃蛋,从而使得蛇变得越来越长。通过上下左右方向键控制蛇的方向,寻找吃的东西,每吃一口就能得到一定的积分。本例难度为中级,适合具有 Python 基础和 Pygame 编程知识的用户学习。
设计要点
游戏是基于 PyGame 框架制作的,程序核心逻辑如下:游戏界面分辨率是 640480,蛇和食物都是由 1 个或多个 2020 像素的正方形块儿组成。初始化时蛇的长度是 3,食物是 1 个点。游戏开始后,根据蛇的当前移动方向,将蛇运动方向的前方的那个点 append 到蛇数组的末位,再把蛇尾去掉。如果蛇吃到了食物,即蛇头的坐标等于食物的坐标,那么在第 2 点中蛇尾就不用去掉,就产生了蛇长度增加的效果。当蛇撞上自身或墙壁,游戏结束。
示例源码
import pygame
from os import path
from sys import exit
from time import sleep
from random import choice
from itertools import product
from pygame.locals import QUIT, KEYDOWN
def direction_check(moving_direction, change_direction):
directions = [['up', 'down'], ['left', 'right']]
if moving_direction in directions[0] and change_direction in directions[1]:
return change_direction
elif moving_direction in directions[1] and change_direction in directions[0]:
return change_direction
return moving_direction
class Snake:
colors = list(product([0, 64, 128, 192, 255], repeat=3))[1:-1]
def __init__(self):
self.map = {(x, y): 0 for x in range(32) for y in range(24)}
self.body = [[100, 100], [120, 100], [140, 100]]
self.head = [140, 100]
self.food = []
self.food_color = []
self.moving_direction = 'right'
self.speed = 4
self.generate_food()
self.game_started = False
def check_game_status(self):
if self.body.count(self.head) > 1:
return True
if self.head[0] < 0 or self.head[0] > 620 or self.head[1] < 0 or self.head[1] > 460:
return True
return False
def move_head(self):
moves = {
'right': (20, 0),
'up': (0, -20),
'down': (0, 20),
'left': (-20, 0)
}
step = moves[self.moving_direction]
self.head[0] += step[0]
self.head[1] += step[1]
def generate_food(self):
self.speed = len(self.body) // 16 if len(self.body) // 16 > 4 else self.speed
for seg in self.body:
x, y = seg
self.map[x // 20, y // 20] = 1
empty_pos = [pos for pos in self.map.keys() if not self.map[pos]]
result = choice(empty_pos)
self.food_color = list(choice(self.colors))
self.food = [result[0] * 20, result[1] * 20]
def main():
key_direction_dict = {
119: 'up', 115: 'down', 97: 'left', 100: 'right',
273: 'up', 274: 'down', 276: 'left', 275: 'right',
}
fps_clock = pygame.time.Clock()
pygame.init()
pygame.mixer.init()
snake = Snake()
sound = False
title_font = pygame.font.SysFont('simsunnsimsun', 32)
welcome_words = title_font.render('贪吃蛇', True, (0, 0, 0), (255, 255, 255))
tips_font = pygame.font.SysFont('simsunnsimsun', 20)
start_game_words = tips_font.render('点击开始', True, (0, 0, 0), (255, 255, 255))
close_game_words = tips_font.render('按 ESC 退出', True, (0, 0, 0), (255, 255, 255))
gameover_words = title_font.render('游戏结束', True, (205, 92, 92), (255, 255, 255))
win_words = title_font.render('蛇很长了,你赢了!', True, (0, 0, 205), (255, 255, 255))
screen = pygame.display.set_mode((640, 480), 0, 32)
pygame.display.set_caption('贪吃蛇')
new_direction = snake.moving_direction
while 1:
for event in pygame.event.get():
if event.type == QUIT:
exit()
elif event.type == KEYDOWN:
if event.key == 27:
exit()
if snake.game_started and event.key in key_direction_dict:
direction = key_direction_dict[event.key]
new_direction = direction_check(snake.moving_direction, direction)
elif (not snake.game_started) and event.type == pygame.MOUSEBUTTONDOWN:
x, y = pygame.mouse.get_pos()
if 213 <= x <= 422 and 304 <= y <= 342:
snake.game_started = True
screen.fill((255, 255, 255))
if snake.game_started:
snake.moving_direction = new_direction
snake.move_head()
snake.body.append(snake.head[:])
if snake.head == snake.food:
snake.generate_food()
else:
snake.body.pop(0)
for seg in snake.body:
pygame.draw.rect(screen, [0, 0, 0], [seg[0], seg[1], 20, 20], 0)
pygame.draw.rect(screen, snake.food_color, [snake.food[0], snake.food[1], 20, 20], 0)
if snake.check_game_status():
screen.blit(gameover_words, (241, 310))
pygame.display.update()
snake = Snake()
new_direction = snake.moving_direction
sleep(3)
elif len(snake.body) == 512:
screen.blit(win_words, (33, 210))
pygame.display.update()
snake = Snake()
new_direction = snake.moving_direction
sleep(3)
else:
screen.blit(welcome_words, (240, 150))
screen.blit(start_game_words, (246, 310))
screen.blit(close_game_words, (246, 350))
pygame.display.update()
fps_clock.tick(snake.speed)
if __name__ == '__main__':
main()
5. 俄罗斯方块
案例介绍
俄罗斯方块是由 4 个小方块组成不同形状的板块,随机从屏幕上方落下,按方向键调整板块的位置和方向。这些完整的横条会消失,给新落下来的板块腾出空间,并获得分数奖励。没有被消除掉的方块不断堆积,一旦堆到顶端,便告输,游戏结束。
设计要点
边框由 15*25 个空格组成,盒子是组成方块的其中小方块,是组成方块的基本单元。每个方块由 4 个盒子组成。形状有 T, S, Z, J, L, I, O 等。当一个方块到达边框的底部或接触到在其他的盒子话,就说这个方块着陆了。那样的话,另一个方块就会开始下落。
示例源码
import pygame
import random
import os
pygame.init()
GRID_WIDTH = 20
GRID_NUM_WIDTH = 15
GRID_NUM_HEIGHT = 25
WIDTH, HEIGHT = GRID_WIDTH * GRID_NUM_WIDTH, GRID_WIDTH * GRID_NUM_HEIGHT
SIDE_WIDTH = 200
SCREEN_WIDTH = WIDTH + SIDE_WIDTH
WHITE = (0xff, 0xff, 0xff)
BLACK = (0, 0, 0)
LINE_COLOR = (0x33, 0x33, 0x33)
CUBE_COLORS = [
(0xcc, 0x99, 0x99), (0xff, 0xff, 0x99), (0x66, 0x66, 0x99),
(0x99, 0x00, 0x66), (0xff, 0xcc, 0x00), (0xcc, 0x00, 0x33),
(0xff, 0x00, 0x33), (0x00, 0x66, 0x99), (0xff, 0xff, 0x33),
(0x99, 0x00, 0x33), (0xcc, 0xff, 0x66), (0xff, 0x99, 0x00)
]
screen = pygame.display.set_mode((SCREEN_WIDTH, HEIGHT))
pygame.display.set_caption("俄罗斯方块")
clock = pygame.time.Clock()
FPS = 30
score = 0
level = 1
screen_color_matrix = [[None] * GRID_NUM_WIDTH for i in range(GRID_NUM_HEIGHT)]
base_folder = os.path.dirname(__file__)
def show_text(surf, text, size, x, y, color=WHITE):
font_name = os.path.join(base_folder, 'font/font.ttc')
try:
font = pygame.font.Font(font_name, size)
except:
font = pygame.font.SysFont(None, size)
text_surface = font.render(text, True, color)
text_rect = text_surface.get_rect()
text_rect.midtop = (x, y)
surf.blit(text_surface, text_rect)
class CubeShape(object):
SHAPES = ['I', 'J', 'L', 'O', 'S', 'T', 'Z']
I = [[(0, -1), (0, 0), (0, 1), (0, 2)], [(-1, 0), (0, 0), (1, 0), (2, 0)]]
J = [[(-2, 0), (-1, 0), (0, 0), (0, -1)], [(-1, 0), (0, 0), (0, 1), (0, 2)], [(0, 1), (0, 0), (1, 0), (2, 0)], [(0, -2), (0, -1), (0, 0), (1, 0)]]
L = [[(-2, 0), (-1, 0), (0, 0), (0, 1)], [(1, 0), (0, 0), (0, 1), (0, 2)], [(0, -1), (0, 0), (1, 0), (2, 0)], [(0, -2), (0, -1), (0, 0), (-1, 0)]]
O = [[(0, 0), (0, 1), (1, 0), (1, 1)]]
S = [[(-1, 0), (0, 0), (0, 1), (1, 1)], [(1, -1), (1, 0), (0, 0), (0, 1)]]
T = [[(0, -1), (0, 0), (0, 1), (-1, 0)], [(-1, 0), (0, 0), (1, 0), (0, 1)], [(0, -1), (0, 0), (0, 1), (1, 0)], [(-1, 0), (0, 0), (1, 0), (0, -1)]]
Z = [[(0, -1), (0, 0), (1, 0), (1, 1)], [(-1, 0), (0, 0), (0, -1), (1, -1)]]
SHAPES_WITH_DIR = {'I': I, 'J': J, 'L': L, 'O': O, 'S': S, 'T': T, 'Z': Z}
def __init__(self):
self.shape = self.SHAPES[random.randint(0, len(self.SHAPES) - 1)]
self.center = (2, GRID_NUM_WIDTH // 2)
self.dir = random.randint(0, len(self.SHAPES_WITH_DIR[self.shape]) - 1)
self.color = CUBE_COLORS[random.randint(0, len(CUBE_COLORS) - 1)]
def get_all_gridpos(self, center=None):
curr_shape = self.SHAPES_WITH_DIR[self.shape][self.dir]
if center is None:
center = [self.center[0], self.center[1]]
return [(cube[0] + center[0], cube[1] + center[1]) for cube in curr_shape]
def conflict(self, center):
for cube in self.get_all_gridpos(center):
if cube[0] < 0 or cube[1] < 0 or cube[0] >= GRID_NUM_HEIGHT or cube[1] >= GRID_NUM_WIDTH:
return True
if screen_color_matrix[cube[0]][cube[1]] is not None:
return True
return False
def rotate(self):
new_dir = self.dir + 1
new_dir %= len(self.SHAPES_WITH_DIR[self.shape])
old_dir = self.dir
self.dir = new_dir
if self.conflict(self.center):
self.dir = old_dir
return False
def down(self):
center = (self.center[0] + 1, self.center[1])
if self.conflict(center):
return False
self.center = center
return True
def left(self):
center = (self.center[0], self.center[1] - 1)
if self.conflict(center):
return False
self.center = center
return True
def right(self):
center = (self.center[0], self.center[1] + 1)
if self.conflict(center):
return False
self.center = center
return True
def draw(self):
for cube in self.get_all_gridpos():
pygame.draw.rect(screen, self.color, (cube[1] * GRID_WIDTH, cube[0] * GRID_WIDTH, GRID_WIDTH, GRID_WIDTH))
pygame.draw.rect(screen, WHITE, (cube[1] * GRID_WIDTH, cube[0] * GRID_WIDTH, GRID_WIDTH, GRID_WIDTH), 1)
def draw_grids():
for i in range(GRID_NUM_WIDTH):
pygame.draw.line(screen, LINE_COLOR, (i * GRID_WIDTH, 0), (i * GRID_WIDTH, HEIGHT))
for i in range(GRID_NUM_HEIGHT):
pygame.draw.line(screen, LINE_COLOR, (0, i * GRID_WIDTH), (WIDTH, i * GRID_WIDTH))
pygame.draw.line(screen, WHITE, (GRID_WIDTH * GRID_NUM_WIDTH, 0), (GRID_WIDTH * GRID_NUM_WIDTH, GRID_WIDTH * GRID_NUM_HEIGHT))
def draw_matrix():
for i, row in zip(range(GRID_NUM_HEIGHT), screen_color_matrix):
for j, color in zip(range(GRID_NUM_WIDTH), row):
if color is not None:
pygame.draw.rect(screen, color, (j * GRID_WIDTH, i * GRID_WIDTH, GRID_WIDTH, GRID_WIDTH))
pygame.draw.rect(screen, WHITE, (j * GRID_WIDTH, i * GRID_WIDTH, GRID_WIDTH, GRID_WIDTH), 2)
def draw_score():
show_text(screen, u'得分:{}'.format(score), 20, WIDTH + SIDE_WIDTH // 2, 100)
def remove_full_line():
global screen_color_matrix, score, level
new_matrix = [[None] * GRID_NUM_WIDTH for i in range(GRID_NUM_HEIGHT)]
index = GRID_NUM_HEIGHT - 1
n_full_line = 0
for i in range(GRID_NUM_HEIGHT - 1, -1, -1):
is_full = True
for j in range(GRID_NUM_WIDTH):
if screen_color_matrix[i][j] is None:
is_full = False
continue
if not is_full:
new_matrix[index] = screen_color_matrix[i]
index -= 1
else:
n_full_line += 1
score += n_full_line
level = score // 20 + 1
screen_color_matrix = new_matrix
def show_welcome(screen):
show_text(screen, u'俄罗斯方块', 30, WIDTH / 2, HEIGHT / 2)
show_text(screen, u'按任意键开始游戏', 20, WIDTH / 2, HEIGHT / 2 + 50)
running = True
gameover = True
counter = 0
live_cube = None
while running:
clock.tick(FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if gameover:
gameover = False
live_cube = CubeShape()
break
if event.key == pygame.K_LEFT:
live_cube.left()
elif event.key == pygame.K_RIGHT:
live_cube.right()
elif event.key == pygame.K_DOWN:
live_cube.down()
elif event.key == pygame.K_UP:
live_cube.rotate()
elif event.key == pygame.K_SPACE:
while live_cube.down() == True:
pass
remove_full_line()
if gameover is False and counter % (FPS // level) == 0:
if live_cube.down() == False:
for cube in live_cube.get_all_gridpos():
screen_color_matrix[cube[0]][cube[1]] = live_cube.color
live_cube = CubeShape()
if live_cube.conflict(live_cube.center):
gameover = True
score = 0
live_cube = None
screen_color_matrix = [[None] * GRID_NUM_WIDTH for i in range(GRID_NUM_HEIGHT)]
remove_full_line()
counter += 1
screen.fill(BLACK)
draw_grids()
draw_matrix()
draw_score()
if live_cube is not None:
live_cube.draw()
if gameover:
show_welcome(screen)
pygame.display.update()
6. 连连看游戏
案例介绍
连连看是一款曾经非常流行的小游戏。游戏规则为点击选中两个相同的方块,两个选中的方块之间连接线的折点不超过两个。每找出一对,它们就会自动消失。连线不能从尚未消失的图案上经过。把所有的图案全部消除即可获得胜利。
设计思路
生成成对的图片元素,将图片元素打乱排布。定义什么才算相连(两张图片的连线不多于 3 跟直线,或者说转角不超过 2 个)。实现相连判断算法,消除图片元素并判断是否消除完毕。
示例源码
from tkinter import *
from tkinter.messagebox import *
from threading import Timer
import time
import random
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def IsLink(p1, p2):
if lineCheck(p1, p2):
return True
if OneCornerLink(p1, p2):
return True
if TwoCornerLink(p1, p2):
return True
return False
def IsSame(p1, p2):
if map[p1.x][p1.y] == map[p2.x][p2.y]:
print("clicked at IsSame")
return True
return False
def callback(event):
global Select_first, p1, p2
global firstSelectRectId, SecondSelectRectId
x = (event.x) // 40
y = (event.y) // 40
if map[x][y] == " ":
showinfo(title="提示", message="此处无方块")
else:
if Select_first == False:
p1 = Point(x, y)
firstSelectRectId = cv.create_rectangle(x * 40, y * 40, x * 40 + 40, y * 40 + 40, width=2, outline="blue")
Select_first = True
else:
p2 = Point(x, y)
if (p1.x == p2.x) and (p1.y == p2.y):
return
SecondSelectRectId = cv.create_rectangle(x * 40, y * 40, x * 40 + 40, y * 40 + 40, width=2, outline="yellow")
cv.pack()
if IsSame(p1, p2) and IsLink(p1, p2):
Select_first = False
drawLinkLine(p1, p2)
t = Timer(timer_interval, delayrun)
t.start()
else:
cv.delete(firstSelectRectId)
cv.delete(SecondSelectRectId)
Select_first = False
timer_interval = 0.3
def delayrun():
clearTwoBlock()
def clearTwoBlock():
cv.delete(firstSelectRectId)
cv.delete(SecondSelectRectId)
map[p1.x][p1.y] = " "
cv.delete(image_map[p1.x][p1.y])
map[p2.x][p2.y] = " "
cv.delete(image_map[p2.x][p2.y])
Select_first = False
undrawConnectLine()
def drawQiPan():
for i in range(0, 15):
cv.create_line(20, 20 + 40 * i, 580, 20 + 40 * i, width=2)
for i in range(0, 15):
cv.create_line(20 + 40 * i, 20, 20 + 40 * i, 580, width=2)
cv.pack()
def print_map():
global image_map
for x in range(0, Width):
for y in range(0, Height):
if (map[x][y] != ' '):
img1 = imgs[int(map[x][y])]
id = cv.create_image((x * 40 + 20, y * 40 + 20), image=img1)
image_map[x][y] = id
cv.pack()
def lineCheck(p1, p2):
absDistance = 0
spaceCount = 0
if (p1.x == p2.x or p1.y == p2.y):
if (p1.x == p2.x and p1.y != p2.y):
absDistance = abs(p1.y - p2.y) - 1
zf = -1 if p1.y - p2.y > 0 else 1
for i in range(1, absDistance + 1):
if (map[p1.x][p1.y + i * zf] == " "):
spaceCount += 1
else:
break
elif (p1.y == p2.y and p1.x != p2.x):
absDistance = abs(p1.x - p2.x) - 1
zf = -1 if p1.x - p2.x > 0 else 1
for i in range(1, absDistance + 1):
if (map[p1.x + i * zf][p1.y] == " "):
spaceCount += 1
else:
break
if (spaceCount == absDistance):
return True
else:
return False
else:
return False
def OneCornerLink(p1, p2):
checkP = Point(p1.x, p2.y)
checkP2 = Point(p2.x, p1.y)
if (map[checkP.x][checkP.y] == " "):
if (lineCheck(p1, checkP) and lineCheck(checkP, p2)):
linePointStack.append(checkP)
return True
if (map[checkP2.x][checkP2.y] == " "):
if (lineCheck(p1, checkP2) and lineCheck(checkP2, p2)):
linePointStack.append(checkP2)
return True
return False
def TwoCornerLink(p1, p2):
checkP = Point(p1.x, p1.y)
for i in range(0, 4):
checkP.x = p1.x
checkP.y = p1.y
if (i == 3):
checkP.y += 1
while ((checkP.y < Height) and map[checkP.x][checkP.y] == " "):
linePointStack.append(checkP)
if (OneCornerLink(checkP, p2)):
return True
else:
linePointStack.pop()
checkP.y += 1
elif (i == 2):
checkP.x += 1
while ((checkP.x < Width) and map[checkP.x][checkP.y] == " "):
linePointStack.append(checkP)
if (OneCornerLink(checkP, p2)):
return True
else:
linePointStack.pop()
checkP.x += 1
elif (i == 1):
checkP.x -= 1
while ((checkP.x >= 0) and map[checkP.x][checkP.y] == " "):
linePointStack.append(checkP)
if (OneCornerLink(checkP, p2)):
return True
else:
linePointStack.pop()
checkP.x -= 1
elif (i == 0):
checkP.y -= 1
while ((checkP.y >= 0) and map[checkP.x][checkP.y] == " "):
linePointStack.append(checkP)
if (OneCornerLink(checkP, p2)):
return True
else:
linePointStack.pop()
checkP.y -= 1
return False
def drawLinkLine(p1, p2):
if (len(linePointStack) == 0):
Line_id.append(drawLine(p1, p2))
else:
if (len(linePointStack) == 1):
z = linePointStack.pop()
Line_id.append(drawLine(p1, z))
Line_id.append(drawLine(p2, z))
if (len(linePointStack) == 2):
z1 = linePointStack.pop()
Line_id.append(drawLine(p2, z1))
z2 = linePointStack.pop()
Line_id.append(drawLine(z1, z2))
Line_id.append(drawLine(p1, z2))
def undrawConnectLine():
while len(Line_id) > 0:
idpop = Line_id.pop()
cv.delete(idpop)
def drawLine(p1, p2):
id = cv.create_line(p1.x * 40 + 20, p1.y * 40 + 20, p2.x * 40 + 20, p2.y * 40 + 20, width=5, fill='red')
return id
def create_map():
global map
tmpMap = []
m = (Width) * (Height) // 10
for x in range(0, m):
for i in range(0, 10):
tmpMap.append(x)
random.shuffle(tmpMap)
for x in range(0, Width):
for y in range(0, Height):
map[x][y] = tmpMap[x * Height + y]
root = Tk()
root.title("Python 连连看 ")
imgs = [PhotoImage(file='images\\bar_0' + str(i) + '.gif') for i in range(0, 10)]
Select_first = False
firstSelectRectId = -1
SecondSelectRectId = -1
clearFlag = False
linePointStack = []
Line_id = []
Height = 10
Width = 10
map = [[" " for y in range(Height)] for x in range(Width)]
image_map = [[" " for y in range(Height)] for x in range(Width)]
cv = Canvas(root, bg='green', width=440, height=440)
cv.bind("<Button-1>", callback)
cv.pack()
create_map()
print_map()
root.mainloop()
总结
以上六个项目涵盖了 Python GUI 编程和游戏开发的基础知识。通过实践这些案例,读者可以深入理解事件驱动编程、图形渲染循环、数据结构应用以及文件持久化等技术点。建议在实际操作中修改代码参数,尝试添加新功能,以巩固所学知识。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- curl 转代码
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online