大家好,我是Jia ming,今天为大家带来 Python GUI 系列 —— 用 tkinter 实现五子棋


文章目录

1. 前言

五子棋的棋具与围棋通用,是一种传统棋种。五子棋容易上手,老少皆宜,而且趣味横生,引人入胜:它不仅能增强思维能力,提高智力,而且富含哲理,有助于修身养性。

该软件主要分为绘制棋子模块、绘制棋盘模块和胜负判定模块三个部分。

2. 效果预览

如图所示,展示了本软件的最终效果,黑方先手,依次落子,界面下方有落子方提示,待五子连线后,会弹出游戏结束的弹窗。
【案例】用 tkinter 实现五子棋(第二版)_python


图0 效果图

3. 重点模块介绍

3.0 数据结构

该软件紧密围绕着一个数据结构:​​chessBoardDict = collections.OrderedDict() #将普通字典转换为有序字典{} {(x, y):"black"}​​,熟悉 Python 的同学都知道,这是一个有序字典,字典键为棋盘横纵坐标,值为棋子颜色(“balck”, “white”)。

Python 字典是无序的,但是可以通过 ​​collections.OrderedDict()​​ 方法构建有序字典,之所以需要有序,是因为该字典保存了落子双方的顺序,有序的保证是必须的。有序字典会严格保证遍历顺序和添加顺序一致。

构造方法:

>>> import collections
>>> x = collections.OrderedDict()
>>> x[1] = 'a'
>>> x[2] = 'b'
>>> x[3] = 'c'
>>> x
OrderedDict([(1, 'a'), (2, 'b'), (3, 'c')])

遍历方法:

>>> for k, v in x.items():
... print(k, v)
...
1 a
2 b
3 c

3.1 绘制棋子模块

通过变量:

rate_piece = 0.45 # 棋子占格子大小的百分比
grid = 40 # 每个格子的大小

方便控制棋子的大小。

模块输入输出?
传入参数:​​​canvas 画布对象​​​、​​需要绘制的坐标​​ 输出:无

如何知道绘制的棋子颜色?

遍历 ​​chessBoardDict ​​ 有序字典,该字典的最后既是前一个落子的玩家,只需要取对手玩家的颜色即可。

def drawPiece(canvas, coordinate:tuple)->None:
"""
coordinate: (x, y)
WB: white/black
需要完成坐标映射
"""
global user
if coordinate[0] % grid >= rate_piece*grid or coordinate[1] % grid >= rate_piece*grid or coordinate[0] >= 16*grid or coordinate[1] >= 16*grid or coordinate[0] < grid or coordinate[1] < grid:
return None
x = coordinate[0] // grid
y = coordinate[1] // grid

if len(chessBoardDict) == 0:
user = 'white'
canvas.create_oval(x*grid+rate_piece*grid, y*grid+rate_piece*grid, x*grid-rate_piece*grid, y*grid-rate_piece*grid, width=1, fill="black")
print("落子: %s (%d, %d)" % ("black", x, y))
return [(x, y), "black"]
color = -1
for k, v in chessBoardDict.items():
_, color = k, v
if color == "black":
user = 'black'
canvas.create_oval(x*grid+rate_piece*grid, y*grid+rate_piece*grid, x*grid-rate_piece*grid, y*grid-rate_piece*grid, fill="white")
print("落子: %s (%d, %d)" % ("white", x, y))
return [(x, y), "white"]
else:
user = 'white'
canvas.create_oval(x*grid+rate_piece*grid, y*grid+rate_piece*grid, x*grid-rate_piece*grid, y*grid-rate_piece*grid, fill="black")
print("落子: %s (%d, %d)" % ("black", x, y))
return [(x, y), "black"]

该模块主要是能够按照顺序在鼠标点击位置的附近棋盘线交叉点绘制出棋子。主要需要考虑棋子的大小、颜色问题,如图1所示,展示了绘制棋子的效果。

【案例】用 tkinter 实现五子棋(第二版)_python_02


图1 绘制棋子

3.2 绘制棋盘模块

该模块需要绘制绘制棋盘外框的边界线、棋盘经纬线(使用全局变量rate来控制大小)、棋盘上黑色辅助点(有rate全局变量来控制大小)。

def drawBoard():
# 界面对象的基本参数设置
root = tk.Tk()
root.title("五子棋")
# root.geometry('335x265+250+250')
# 设置界面是否可以随意拉伸
root.resizable(False, False)

canvas = tk.Canvas(root, bg='#F9D65B', height=grid*16, width=grid*16)
canvas.grid(row=0, column=0)
label = tk.Label(root, text=user, font=('Times', 16)).grid(row=1, column=0)
# 绘制线
canvas.create_rectangle(grid/2, grid/2, 16*grid-grid/2, 16*grid-grid/2, width=2)
for i in range(1, 16):
canvas.create_line(grid, i*grid, 15*grid, i*grid, fill='black')
canvas.create_line(i*grid, grid, i*grid, 15*grid, fill='black')
# 绘制点
canvas.create_oval(8*grid+rate*grid, 8*grid+rate*grid, 8*grid-rate*grid, 8*grid-rate*grid, fill="black")
canvas.create_oval(12*grid+rate*grid, 12*grid+rate*grid, 12*grid-rate*grid, 12*grid-rate*grid, fill="black")
canvas.create_oval(4*grid+rate*grid, 4*grid+rate*grid, 4*grid-rate*grid, 4*grid-rate*grid, fill="black")
canvas.create_oval(12*grid+rate*grid, 4*grid+rate*grid, 12*grid-rate*grid, 4*grid-rate*grid, fill="black")
canvas.create_oval(4*grid+rate*grid, 12*grid+rate*grid, 4*grid-rate*grid, 12*grid-rate*grid, fill="black")
# 鼠标事件
def mouseEvent(event):
# print(f"({event.x}, {event.y})")
value = drawPiece(canvas, (event.x, event.y))
tk.Label(root, text=user, font=('Times', 16)).grid(row=1,column=0)
if value != None:
k, v = value
chessBoardDict[k] = v
judge()
# print(chessBoardDict)
# global k
# k += 1
# canvas.bind("<Button-1>", lambda event:mouseEvent(event, "white" if k % 2 == 0 else "black"))
canvas.bind("<Button-1>", lambda event:mouseEvent(event))

root.mainloop()

按照五子棋盘标准尺寸,绘制15x15条线交叉的棋盘,并标识出棋盘的五个关键点位置。如图2所示,展示了棋盘的绘制效果。

【案例】用 tkinter 实现五子棋(第二版)_右键_03


图2 绘制棋盘

3.3. 胜负判定模块

该模块主要围绕 chessBoardDict 有序字典的遍历,每落子后便调用该模块,判断落子的八个方向是否满足五子连珠的条件。

def judge():
global chessBoardDict
# chessBoardDict.pop()
for k, v in chessBoardDict.items():
coordinate, color = k, v
# print("color", color)
if color == "black":
anti_color = "white"
else:
anti_color = "black"
# print("判断:", chessBoardDict)
points = [0, 0, 0, 0, 0, 0, 0, 0]
for k in range(5):
if (coordinate[0], coordinate[1] - k) in chessBoardDict.keys() and chessBoardDict[(coordinate[0], coordinate[1] - k)] == color:
points[0] += 1
else:
break
for k in range(5):
if (coordinate[0] + k, coordinate[1] - k) in chessBoardDict.keys() and chessBoardDict[(coordinate[0] + k, coordinate[1] - k)] == color:
points[1] += 1
else:
break
for k in range(5):
if (coordinate[0] + k, coordinate[1]) in chessBoardDict.keys() and chessBoardDict[(coordinate[0] + k, coordinate[1])] == color:
points[2] += 1
else:
break
for k in range(5):
if (coordinate[0] + k, coordinate[1] + k) in chessBoardDict.keys() and chessBoardDict[(coordinate[0] + k, coordinate[1] + k)] == color:
points[3] += 1
else:
break
for k in range(5):
if (coordinate[0], coordinate[1] + k) in chessBoardDict.keys() and chessBoardDict[(coordinate[0], coordinate[1] + k)] == color:
points[4] += 1
else:
break
for k in range(5):
if (coordinate[0] - k, coordinate[1] + k) in chessBoardDict.keys() and chessBoardDict[(coordinate[0] - k, coordinate[1] + k)] == color:
points[5] += 1
else:
break
for k in range(5):
if (coordinate[0] - k, coordinate[1]) in chessBoardDict.keys() and chessBoardDict[(coordinate[0] - k, coordinate[1])] == color:
points[6] += 1
else:
break
for k in range(5):
if (coordinate[0] - k, coordinate[1] - k) in chessBoardDict.keys() and chessBoardDict[(coordinate[0] - k, coordinate[1] - k)] == color:
points[7] += 1
else:
break
if 5 in points:
messagebox.showinfo(title="结果", message="%s 获胜!" % color)
print(color, "win")

每当落一个子的时候,就需要判断该棋子的八个方向是否产生了连珠五子的效果,如图3所示需要判断的八个方向。如果成功连珠五子,则结束游戏。
【案例】用 tkinter 实现五子棋(第二版)_python_04


图3 胜负判定的8个方向

3.4 tkinter 知识点

1. 布局

涉及两个对象:canvas 和 label。这也是我们在界面所看到的两个控件,前者为棋盘画布,我们可以在上面绘制图案,后者为棋盘下方label控件,提示每回合落子方。

使用 ​​grid​​ 布局方式。

canvas = tk.Canvas(root, bg='#F9D65B', height=grid*16, width=grid*16)
canvas.grid(row=0, column=0)
label = tk.Label(root, text=user, font=('Times', 16)).grid(row=1,column=0)

关于 grid 的使用方式,下面这个例子可以很好的说明:

from tkinter import *

tk=Tk()
#标签控件,显示文本和位图,展示在第一行
Label(tk,text="First").grid(row=0,sticky=E)#靠右
Label(tk,text="Second").grid(row=2,sticky=W)#第二行,靠左

#输入控件
Entry(tk).grid(row=0,column=1)
Entry(tk).grid(row=2,column=1)

#主事件循环
mainloop()

【案例】用 tkinter 实现五子棋(第二版)_tkinter_05


grid 样例

2. 鼠标事件

如下样例,我们便可以绑定一个鼠标单击左键事件,传入 event 参数,每点一次鼠标,就会打印鼠标的坐标值,我们可以通过这个方法来调用胜负判断模块。

# 鼠标事件
def mouseEvent(event):
print(f"({event.x}, {event.y})")
canvas.bind("<Button-1>", lambda event:mouseEvent(event))

其它鼠标事件:

'''
鼠标点击事件
<Button-1> 鼠标左键
<Button-2> 鼠标中间键(滚轮)
<Button-3> 鼠标右键
<Double-Button-1> 双击鼠标左键
<Double-Button-3> 双击鼠标右键
<Triple-Button-1> 三击鼠标左键
<Triple-Button-3> 三击鼠标右键
'''

3. canvas 画布操作

关于canva画布的操作,我们这里用到了线、矩形、圆形。下面提供了几个学习样例以供参考。

import tkinter as tk

root = tk.Tk()

w = tk.Canvas(root, width =200, height = 100)
w.pack()

#画一条黄色的横线
w.create_line(0, 50, 200, 50, fill = "yellow")
#画一条红色的竖线(虚线)
w.create_line(100, 0, 100, 100, fill = "red", dash = (4, 4))
#中间画一个蓝色的矩形
w.create_rectangle(50, 25, 150, 75, fill = "blue")

root.mainloop()

【案例】用 tkinter 实现五子棋(第二版)_右键_06


线条以及矩形样例

import tkinter as tk

root = tk.Tk()

w = tk.Canvas(root, width =200, height = 100)
w.pack()

w.create_rectangle(40, 20, 160, 80, dash = (4, 4))
w.create_oval(40, 20, 160, 80, fill = "pink")

w.create_text(100, 50, text = "Python")

root.mainloop()

【案例】用 tkinter 实现五子棋(第二版)_鼠标事件_07


圆形样例

4. 开发运行环境

4.1 开发环境

本软件的开发环境如下:

PyCharm 2020.2 (Community Edition)
Build #PC-202.6397.98, built on July 28, 2020
Runtime version: 11.0.7+10-b944.20 amd64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
Windows 10 10.0
GC: ParNew, ConcurrentMarkSweep
Memory: 1902M
Cores: 12

4.2 软件所涉及到的开源库

import tkinter as tk
from tkinter import messagebox
import collections


表1 软件所涉及到的开源库

开源库

说明

tkinter

Python3提供的界面设计库

collections

Python内建的一个集合模块,提供了许多有用的集合类

5. 使用说明

本软件操作简单,界面简洁。
运行程序,用户便能看到如图4所示的界面。
【案例】用 tkinter 实现五子棋(第二版)_python_08


图4 初始界面

下方“black”显示了当前落子的颜色,开局黑方先手。鼠标点击落子即可,落子效果如图5所示。
【案例】用 tkinter 实现五子棋(第二版)_鼠标事件_09


图5 黑方先手

然后是白方落子,如图6所示。
【案例】用 tkinter 实现五子棋(第二版)_python_10


图6 白方落子

如图7所示,黑方获胜,会弹出提示框。

【案例】用 tkinter 实现五子棋(第二版)_tkinter_11


图7 黑方获胜

源码

# 五子棋
# @author: jiaming
import tkinter as tk
from tkinter import messagebox
import collections

rate = 0.1 # 黑点占格子大小的百分比
rate_piece = 0.45 # 棋子占格子大小的百分比
grid = 40 # 每个格子的大小
color = (249, 214, 91) # 棋盘颜色
user = 'black'
# k = 0
chessBoardDict = collections.OrderedDict() #将普通字典转换为有序字典{} {(x, y):"black"}

def judge():
global chessBoardDict
# chessBoardDict.pop()
for k, v in chessBoardDict.items():
coordinate, color = k, v
# print("color", color)
if color == "black":
anti_color = "white"
else:
anti_color = "black"
# print("判断:", chessBoardDict)
points = [0, 0, 0, 0, 0, 0, 0, 0]
for k in range(5):
if (coordinate[0], coordinate[1] - k) in chessBoardDict.keys() and chessBoardDict[(coordinate[0], coordinate[1] - k)] == color:
points[0] += 1
else:
break
for k in range(5):
if (coordinate[0] + k, coordinate[1] - k) in chessBoardDict.keys() and chessBoardDict[(coordinate[0] + k, coordinate[1] - k)] == color:
points[1] += 1
else:
break
for k in range(5):
if (coordinate[0] + k, coordinate[1]) in chessBoardDict.keys() and chessBoardDict[(coordinate[0] + k, coordinate[1])] == color:
points[2] += 1
else:
break
for k in range(5):
if (coordinate[0] + k, coordinate[1] + k) in chessBoardDict.keys() and chessBoardDict[(coordinate[0] + k, coordinate[1] + k)] == color:
points[3] += 1
else:
break
for k in range(5):
if (coordinate[0], coordinate[1] + k) in chessBoardDict.keys() and chessBoardDict[(coordinate[0], coordinate[1] + k)] == color:
points[4] += 1
else:
break
for k in range(5):
if (coordinate[0] - k, coordinate[1] + k) in chessBoardDict.keys() and chessBoardDict[(coordinate[0] - k, coordinate[1] + k)] == color:
points[5] += 1
else:
break
for k in range(5):
if (coordinate[0] - k, coordinate[1]) in chessBoardDict.keys() and chessBoardDict[(coordinate[0] - k, coordinate[1])] == color:
points[6] += 1
else:
break
for k in range(5):
if (coordinate[0] - k, coordinate[1] - k) in chessBoardDict.keys() and chessBoardDict[(coordinate[0] - k, coordinate[1] - k)] == color:
points[7] += 1
else:
break
if 5 in points:
messagebox.showinfo(title="结果", message="%s 获胜!" % color)
print(color, "win")

def drawPiece(canvas, coordinate:tuple)->None:
"""
coordinate: (x, y)
WB: white/black
需要完成坐标映射
"""
global user
if coordinate[0] % grid >= rate_piece*grid or coordinate[1] % grid >= rate_piece*grid or coordinate[0] >= 16*grid or coordinate[1] >= 16*grid or coordinate[0] < grid or coordinate[1] < grid:
return None
x = coordinate[0] // grid
y = coordinate[1] // grid

if len(chessBoardDict) == 0:
user = 'white'
canvas.create_oval(x*grid+rate_piece*grid, y*grid+rate_piece*grid, x*grid-rate_piece*grid, y*grid-rate_piece*grid, width=1, fill="black")
print("落子: %s (%d, %d)" % ("black", x, y))
return [(x, y), "black"]
color = -1
for k, v in chessBoardDict.items():
_, color = k, v
if color == "black":
user = 'black'
canvas.create_oval(x*grid+rate_piece*grid, y*grid+rate_piece*grid, x*grid-rate_piece*grid, y*grid-rate_piece*grid, fill="white")
print("落子: %s (%d, %d)" % ("white", x, y))
return [(x, y), "white"]
else:
user = 'white'
canvas.create_oval(x*grid+rate_piece*grid, y*grid+rate_piece*grid, x*grid-rate_piece*grid, y*grid-rate_piece*grid, fill="black")
print("落子: %s (%d, %d)" % ("black", x, y))
return [(x, y), "black"]

def drawBoard():
# 界面对象的基本参数设置
root = tk.Tk()
root.title("五子棋")
# root.geometry('335x265+250+250')
# 设置界面是否可以随意拉伸
root.resizable(False, False)

canvas = tk.Canvas(root, bg='#F9D65B', height=grid*16, width=grid*16)
canvas.grid(row=0, column=0)
label = tk.Label(root, text=user, font=('Times', 16)).grid(row=1, column=0)
# 绘制线
canvas.create_rectangle(grid/2, grid/2, 16*grid-grid/2, 16*grid-grid/2, width=2)
for i in range(1, 16):
canvas.create_line(grid, i*grid, 15*grid, i*grid, fill='black')
canvas.create_line(i*grid, grid, i*grid, 15*grid, fill='black')
# 绘制点
canvas.create_oval(8*grid+rate*grid, 8*grid+rate*grid, 8*grid-rate*grid, 8*grid-rate*grid, fill="black")
canvas.create_oval(12*grid+rate*grid, 12*grid+rate*grid, 12*grid-rate*grid, 12*grid-rate*grid, fill="black")
canvas.create_oval(4*grid+rate*grid, 4*grid+rate*grid, 4*grid-rate*grid, 4*grid-rate*grid, fill="black")
canvas.create_oval(12*grid+rate*grid, 4*grid+rate*grid, 12*grid-rate*grid, 4*grid-rate*grid, fill="black")
canvas.create_oval(4*grid+rate*grid, 12*grid+rate*grid, 4*grid-rate*grid, 12*grid-rate*grid, fill="black")
# 鼠标事件
def mouseEvent(event):
# print(f"({event.x}, {event.y})")
value = drawPiece(canvas, (event.x, event.y))
tk.Label(root, text=user, font=('Times', 16)).grid(row=1,column=0)
if value != None:
k, v = value
chessBoardDict[k] = v
judge()
# print(chessBoardDict)
# global k
# k += 1
# canvas.bind("<Button-1>", lambda event:mouseEvent(event, "white" if k % 2 == 0 else "black"))
canvas.bind("<Button-1>", lambda event:mouseEvent(event))

root.mainloop()

if __name__ == "__main__":
drawBoard()
'''
鼠标点击事件
<Button-1> 鼠标左键
<Button-2> 鼠标中间键(滚轮)
<Button-3> 鼠标右键
<Double-Button-1> 双击鼠标左键
<Double-Button-3> 双击鼠标右键
<Triple-Button-1> 三击鼠标左键
<Triple-Button-3> 三击鼠标右键
'''