Python 图形界面与游戏开发实战
本文介绍六个基于 Python 的实用项目,涵盖 Tkinter 图形界面编程与 Pygame 游戏开发。内容包括简易计算器、文本编辑器、用户登录注册模块,以及贪吃蛇、俄罗斯方块和连连看等经典游戏的实现原理与核心代码。
使用 Python 进行图形界面和游戏开发的六个实战项目,包括基于 Tkinter 的计算器、记事本、用户登录注册模块,以及基于 Pygame 的贪吃蛇、俄罗斯方块和连连看游戏。内容详细阐述了各项目的实现原理、核心逻辑与完整代码,重点讲解了界面布局、事件处理、文件序列化存储及游戏状态机设计,适合具备 Python 基础的学习者参考实践。

本文介绍六个基于 Python 的实用项目,涵盖 Tkinter 图形界面编程与 Pygame 游戏开发。内容包括简易计算器、文本编辑器、用户登录注册模块,以及贪吃蛇、俄罗斯方块和连连看等经典游戏的实现原理与核心代码。
本例利用 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=self.myfunc)
allmenu.add_cascade(label='查看', menu=filemenu)
editmenu = tkinter.Menu(allmenu, tearoff=0)
editmenu.add_command(label='复制', command=self.myfunc)
editmenu.add_command(label='粘贴', command=self.myfunc)
allmenu.add_cascade(label='编辑', menu=editmenu)
helpmenu = tkinter.Menu(allmenu, tearoff=0)
helpmenu.add_command(label='帮助', command=self.myfunc)
allmenu.add_cascade(label='帮助', menu=helpmenu)
self.root.config(menu=allmenu)
def layout(self):
show_label = tkinter.Label(self.root, bd=3, bg='white', font=('宋体', 30), anchor='e', textvariable=self.result)
show_label.place(x=5, y=20, width=270, height=70)
# 功能按钮 MC
tkinter.Button(self.root, text='MC', command=self.wait).place(x=5, y=95, width=50, height=50)
# 功能按钮 MR
tkinter.Button(self.root, text='MR', command=self.wait).place(x=60, y=95, width=50, height=50)
# 功能按钮 MS
tkinter.Button(self.root, text='MS', command=self.wait).place(x=115, y=95, width=50, height=50)
# 功能按钮 M+
tkinter.Button(self.root, text='M+', command=self.wait).place(x=170, y=95, width=50, height=50)
# 功能按钮 M-
tkinter.Button(self.root, text='M-', command=self.wait).place(x=225, y=95, width=50, height=50)
# 退格键
tkinter.Button(self.root, text='←', command=self.dele_one).place(x=5, y=150, width=50, height=50)
# CE 键
tkinter.Button(self.root, text='CE', command=lambda: self.result.set(0)).place(x=60, y=150, width=50, height=50)
# C 键
tkinter.Button(self.root, text='C', command=self.sweeppress).place(x=115, y=150, width=50, height=50)
# ±键
tkinter.Button(self.root, text='±', command=self.zf).place(x=170, y=150, width=50, height=50)
# √键
tkinter.Button(self.root, text='√', command=self.kpf).place(x=225, y=150, width=50, height=50)
# 数字 7
tkinter.Button(self.root, text='7', command=lambda: self.pressnum('7')).place(x=5, y=205, width=50, height=50)
# 数字 8
tkinter.Button(self.root, text='8', command=lambda: self.pressnum('8')).place(x=60, y=205, width=50, height=50)
# 数字 9
tkinter.Button(self.root, text='9', command=lambda: self.pressnum('9')).place(x=115, y=205, width=50, height=50)
# /键
tkinter.Button(self.root, text='/', command=lambda: self.presscalculate('/')).place(x=170, y=205, width=50, height=50)
# //键
tkinter.Button(self.root, text='//', command=lambda: self.presscalculate('//')).place(x=225, y=205, width=50, height=50)
# 数字 4
tkinter.Button(self.root, text='4', command=lambda: self.pressnum('4')).place(x=5, y=260, width=50, height=50)
# 数字 5
tkinter.Button(self.root, text='5', command=lambda: self.pressnum('5')).place(x=60, y=260, width=50, height=50)
# 数字 6
tkinter.Button(self.root, text='6', command=lambda: self.pressnum('6')).place(x=115, y=260, width=50, height=50)
# *键
tkinter.Button(self.root, text='*', command=lambda: self.presscalculate('*')).place(x=170, y=260, width=50, height=50)
# 1/x 键
tkinter.Button(self.root, text='1/x', command=self.ds).place(x=225, y=260, width=50, height=50)
# 数字 1
tkinter.Button(self.root, text='1', command=lambda: self.pressnum('1')).place(x=5, y=315, width=50, height=50)
# 数字 2
tkinter.Button(self.root, text='2', command=lambda: self.pressnum('2')).place(x=60, y=315, width=50, height=50)
# 数字 3
tkinter.Button(self.root, text='3', command=lambda: self.pressnum('3')).place(x=115, y=315, width=50, height=50)
# -键
tkinter.Button(self.root, text='-', command=lambda: self.presscalculate('-')).place(x=170, y=315, width=50, height=50)
# =键
tkinter.Button(self.root, text='=', command=lambda: self.pressequal()).place(x=225, y=315, width=50, height=105)
# 数字 0
tkinter.Button(self.root, text='0', command=lambda: self.pressnum('0')).place(x=5, y=370, width=105, height=50)
# .键
tkinter.Button(self.root, text='.', command=lambda: self.pressnum('.')).place(x=115, y=370, width=50, height=50)
# +键
tkinter.Button(self.root, text='+', command=lambda: self.presscalculate('+')).place(x=170, y=370, width=50, height=50)
def myfunc(self):
tkinter.messagebox.showinfo('', '预留接口')
def pressnum(self, num):
if self.ispresssign == False:
pass
else:
self.result.set(0)
self.ispresssign = False
if num == '.':
num = '0.'
oldnum = self.result.get()
if oldnum == '0':
self.result.set(num)
else:
newnum = oldnum + num
self.result.set(newnum)
def presscalculate(self, sign):
num = self.result.get()
self.lists.append(num)
self.lists.append(sign)
self.ispresssign = True
def pressequal(self):
curnum = self.result.get()
self.lists.append(curnum)
calculatestr = ''.join(self.lists)
endnum = eval(calculatestr)
self.result.set(str(endnum)[:10])
if self.lists != 0:
self.ispresssign = True
self.lists.clear()
def wait(self):
tkinter.messagebox.showinfo('', '更新中...')
def dele_one(self):
if self.result.get() == '' or self.result.get() == '0':
self.result.set('0')
return
else:
num = len(self.result.get())
if num > 1:
strnum = self.result.get()
strnum = strnum[0:num - 1]
self.result.set(strnum)
else:
self.result.set('0')
def zf(self):
strnum = self.result.get()
if strnum[0] == '-':
self.result.set(strnum[1:])
elif strnum[0] != '-' and strnum != '0':
self.result.set('-' + strnum)
def ds(self):
dsnum = 1 / int(self.result.get())
self.result.set(str(dsnum)[:10])
if self.lists != 0:
self.ispresssign = True
self.lists.clear()
def sweeppress(self):
self.lists.clear()
self.result.set(0)
def kpf(self):
strnum = float(self.result.get())
endnum = math.sqrt(strnum)
if str(endnum)[-1] == '0':
self.result.set(str(endnum)[:-2])
else:
self.result.set(str(endnum)[:10])
if self.lists != 0:
self.ispresssign = True
self.lists.clear()
if __name__ == '__main__':
my_calculator = Calculator()
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()
本例设计一个用户登录和注册模块,使用 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()
贪吃蛇是一款经典的益智游戏。该游戏通过控制蛇头方向吃蛋,从而使得蛇变得越来越长。通过上下左右方向键控制蛇的方向,寻找吃的东西,每吃一口就能得到一定的积分。本例难度为中级,适合具有 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()
俄罗斯方块是由 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()
连连看是一款曾经非常流行的小游戏。游戏规则为点击选中两个相同的方块,两个选中的方块之间连接线的折点不超过两个。每找出一对,它们就会自动消失。连线不能从尚未消失的图案上经过。把所有的图案全部消除即可获得胜利。
生成成对的图片元素,将图片元素打乱排布。定义什么才算相连(两张图片的连线不多于 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 编程和游戏开发的基础知识。通过实践这些案例,读者可以深入理解事件驱动编程、图形渲染循环、数据结构应用以及文件持久化等技术点。建议在实际操作中修改代码参数,尝试添加新功能,以巩固所学知识。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online