8.6.3 创建基于tkinter的交互环境
本项目通过tkinter技术实现图形用户界面,未使用者提供了一个交互式的环境,使用户能够轻松地创建地图、选择起点和终点,并使用不同的搜索算法来找到最佳路径。
(1)这段代码定义了类Gui,用于创建程序的图形用户界面(GUI)。用户可以使用它来创建地图、选择城市、设置连接,并选择不同的算法来寻找路径。
class Gui:
def __init__(self):
self.window = root
self.window.title("Path Finder")
self.bg_color = "pink"
self.h_color = "navy"
self.s_color = "RoyalBlue2"
self.c_color = "Magenta2"
self.window.configure(bg=self.bg_color)
self.canvas_size = 700 # canvas
self.canvas = tk.Canvas(root, width=self.canvas_size, height=self.canvas_size, background="white")
self.canvas.grid(column=1, row=0)
self.refresh_canvas()
self.frame = tk.Frame(self.window, bg=self.bg_color)
self.frame.grid(row=0, column=0, sticky="n")
self.frame_list = tk.Frame(self.window, bg=self.bg_color)
self.frame_list.grid(row=0, column=2, sticky="n")
self.city_total = tk.IntVar()
self.input_type = tk.StringVar()
self.city_or_conn = tk.IntVar()
self.algorithm = tk.IntVar()
self.entry = tk.IntVar()
self.dst_src = tk.IntVar()
self.speed = tk.IntVar()
self.evaluation = tk.IntVar()
self.change_type = tk.IntVar()
self.city_count = 0
self.conn_count = 0
self.selected_cities = []
self.ovals = []
self.lines = []
self.gonna_move = -1
self.gonna_move_cons = []
self.city = {
"coords": (0, 0),
"name": "London"
}
self.cities = ["Paris", "Marseille", "Lyon", "Toulouse", "Nice", "Nantes", "Strasbourg", "Montpeiller",
"Bordeaux", "Lille", "Rennas", "Reims", "Le_Havre", "Toulon", "Grenoble", "Dijon", "Angers",
"Villeaurbanne", "Le_Mans", "Saint-Denis"]
self.colors = ["red", "blue", "purple", "brown", "green", "hot pink", "orange red", "black", "cyan",
"deep pink", "green3", "DodgerBlue3", "firebrick4", "LightPink4", "maroon", "SeaGreen3",
"lime green", "navy", "dark violet", "red4"]
header = tk.Label(self.frame, text="Create Map", font='Calibri 12 bold', fg=self.h_color, bg=self.bg_color)
header.grid(column=0, row=0)
section = tk.Label(self.frame, text="Number of Cities", font='Calibri 12 bold', fg=self.s_color,
bg=self.bg_color)
section.grid(column=0, row=1)
cities5 = tk.Radiobutton(self.frame, text='5', variable=self.city_total, command=self.set_connections,
value=5, bg=self.bg_color).grid(column=0, row=2, sticky='w')
cities10 = tk.Radiobutton(self.frame, text='10', variable=self.city_total, command=self.set_connections,
value=10, bg=self.bg_color).grid(column=0, row=2, sticky='n')
cities20 = tk.Radiobutton(self.frame, text='20', variable=self.city_total, command=self.set_connections,
value=20, bg=self.bg_color).grid(column=0, row=2, sticky='e')
section1 = tk.Label(self.frame, text="Number of Connections", font='Calibri 12 bold', fg=self.s_color,
bg=self.bg_color)
section1.grid(column=0, row=5)
entry1 = tk.Entry(self.frame, textvariable=self.entry, background=self.bg_color)
entry1.grid(column=0, row=6)
section2 = tk.Label(self.frame, text="How to Create", font='Calibri 12 bold', fg=self.s_color, bg=self.bg_color)
section2.grid(column=0, row=7)
frame_ran = tk.Radiobutton(self.frame, text='Random', variable=self.input_type, value="Random",
command=self.create_randomly, bg=self.bg_color).grid(column=0, row=8, sticky='w')
frame_us = tk.Radiobutton(self.frame, text='User Input', variable=self.input_type, value="User",
command=self.add_buttons, bg=self.bg_color).grid(column=0, row=8, sticky='e')
section3 = tk.Label(self.frame, text="Cities and Connections", font='Calibri 12 bold', fg=self.s_color,
bg=self.bg_color)
section3.grid(column=0, row=10)
city = tk.Radiobutton(self.frame, text="Add City", variable=self.city_or_conn, command=self.binder, value=0,
bg=self.bg_color).grid(column=0, row=11)
connection = tk.Radiobutton(self.frame, text="Make Connection", variable=self.city_or_conn, command=self.binder,
value=1, bg=self.bg_color).grid(column=0, row=12)
section4 = tk.Label(self.frame, text="Change Map", font='Calibri 12 bold', fg=self.s_color, bg=self.bg_color)
section4.grid(column=0, row=13)
move_city = tk.Radiobutton(self.frame, text='Move City', variable=self.change_type, value=0,
command=self.change_map, bg=self.bg_color).grid(column=0, row=14)
delete_city = tk.Radiobutton(self.frame, text='Delete City', variable=self.change_type, value=1,
command=self.change_map, bg=self.bg_color).grid(column=0, row=15)
delete_conn = tk.Radiobutton(self.frame, text='Delete Connection', variable=self.change_type, value=2,
command=self.change_map, bg=self.bg_color).grid(column=0, row=16)
header2 = tk.Label(self.frame, text="Algorithms", font='Calibri 12 bold', fg=self.h_color, bg=self.bg_color)
header2.grid(column=0, row=17)
alg1 = tk.Radiobutton(self.frame, text='A* Search', variable=self.algorithm, value=0,
bg=self.bg_color).grid(column=0, row=18)
alg2 = tk.Radiobutton(self.frame, text='Best First Search', variable=self.algorithm, value=1,
bg=self.bg_color).grid(column=0, row=19)
alg3 = tk.Radiobutton(self.frame, text='Depth First Search', variable=self.algorithm, value=2,
bg=self.bg_color).grid(column=0, row=20)
alg4 = tk.Radiobutton(self.frame, text='Breadth First Search', variable=self.algorithm, value=3,
bg=self.bg_color).grid(column=0, row=21)
section4 = tk.Label(self.frame, text="Evaluation Function", font='Calibri 12 bold', fg=self.s_color,
bg=self.bg_color)
section4.grid(column=0, row=22)
euclidean = tk.Radiobutton(self.frame, text='Euclidean', variable=self.evaluation, value=0, bg=self.bg_color). \
grid(column=0, row=23, sticky='w')
manhattan = tk.Radiobutton(self.frame, text='Manhattan', variable=self.evaluation, value=1, bg=self.bg_color). \
grid(column=0, row=23, sticky='e')
section3 = tk.Label(self.frame, text="Animation Speed", font='Calibri 12 bold', fg=self.s_color,
bg=self.bg_color)
section3.grid(column=0, row=24)
fast = tk.Radiobutton(self.frame, text='Fast', variable=self.speed, value=0, bg=self.bg_color).\
grid(column=0, row=25, sticky='w')
slow = tk.Radiobutton(self.frame, text='Slow', variable=self.speed, value=1, bg=self.bg_color).\
grid(column=0, row=25, sticky='e')
city_button = tk.Button(self.frame, text="Select Cities", bg=self.s_color, fg=self.h_color,
font='Calibri 12 bold', command=self.select_cities).grid(column=0, row=26,
sticky='e', padx=2)
restart_button = tk.Button(self.frame, text="Restart", bg="firebrick1", fg=self.h_color,
font='Calibri 12 bold', command=self.refresh_canvas).grid(column=0,
row=26, sticky='w')
header3 = tk.Label(self.frame, text="Outputs", font='Calibri 12 bold', fg=self.h_color, bg=self.bg_color)
header3.grid(column=0, row=28)
self.output_label = tk.Label(self.frame, fg=self.h_color, bg=self.bg_color, font='Calibri 12 bold')
self.output_label.grid(column=0, row=29)
self.step_label = tk.Label(self.window, fg=self.h_color, bg=self.bg_color, font='Calibri 16 bold')
self.step_label.grid(column=1, row=1)
'''header2 = tk.Label(self.frame_list, text="", font='Calibri 14 bold', fg=self.h_color, bg=self.bg_color)
header2.grid(column=0, row=0)
distance_labels = tk.Label(self.frame_list, text="", font='Calibri 12 bold', fg=self.s_color, bg=self.bg_color)
distance_labels.grid(column=0, row=1)'''
'''button = tk.Button(self.frame, background=self.bg_color, text="Save", command=self.connection_checker())
button.grid(column=1, row=6)'''
对上述代码的具体说明如下所示。
- 在 __init__ 方法中,设置了窗口的标题、背景色以及一些常用的颜色变量。
- 创建了一个主窗口 root,设置了其标题为 "Path Finder"。
- 创建了一个大小为 700x700 的画布 canvas,用于显示地图,并将其放置在主窗口中央。
- 创建了几个变量,用于存储用户的输入和选择,比如城市数量、连接数量、算法选择等。
- 定义了一些辅助方法,用于处理用户的输入和事件响应,比如创建地图、选择城市、设置连接等。
- 使用 tk.Label 和 tk.Radiobutton 创建了各种标签和单选按钮,用于显示信息和接收用户的输入。
- 创建了一些按钮,比如 "Select Cities" 和 "Restart",用于触发特定操作。
- 创建了一些标签,用于显示输出信息。
(2)下面这段代码定义了一个名为 refresh_canvas 的方法,用于刷新画布的内容。主要功能包括:
- 重新初始化城市列表 cities,其中包含一组默认的城市名称。
- 删除画布上的所有元素,以便清空画布。
- 重新初始化一些变量,包括城市计数器 city_count、连接计数器 conn_count、选定的城市列表 selected_cities、以及绘制的线条列表 lines。
- 在画布上绘制四条边界线,形成一个正方形边框,用于表示地图的边界。
通过调用这个方法,可以在画布上清除之前绘制的内容,并重新绘制边界线,为后续的操作做好准备。
def refresh_canvas(self):
self.cities = ["Paris", "Marseille", "Lyon", "Toulouse", "Nice", "Nantes", "Strasbourg", "Montpeiller",
"Bordeaux", "Lille", "Rennas", "Reims", "Le_Havre", "Toulon", "Grenoble", "Dijon", "Angers",
"Villeaurbanne", "Le_Mans", "Saint-Denis"]
self.canvas.delete("all")
self.ovals = []
self.city_count = 0
self.conn_count = 0
self.selected_cities = []
self.lines = []
self.canvas.create_line(20, 20, 20, 680, fill="gray")
self.canvas.create_line(20, 20, 680, 20, fill="gray")
self.canvas.create_line(680, 20, 680, 680, fill="gray")
self.canvas.create_line(680, 680, 20, 680, fill="gray")
(3)方法load_same 用于重新加载先前保存的地图状态,并在画布上显示相同的城市和连接。它首先清除当前画布上的所有内容,然后重置城市和连接的计数器以及步骤和输出标签的文本。接着,它遍历先前保存的城市列表,为每个城市在画布上创建一个椭圆形代表,并添加城市名称的文本标签。随后,它遍历先前保存的连接列表,为每对连接的城市之间创建一条线,并在连接线的中点添加连接的距离文本。最终,重新加载并显示先前保存的地图状态,包括城市、连接和相关标签信息,以便用户可以继续进行路径查找和操作。
def load_same(self):
self.canvas.delete("all")
self.city_count = 0
self.conn_count = 0
self.canvas.create_line(20, 20, 20, 680, fill="gray")
self.canvas.create_line(20, 20, 680, 20, fill="gray")
self.canvas.create_line(680, 20, 680, 680, fill="gray")
self.canvas.create_line(680, 680, 20, 680, fill="gray")
self.step_label.config(text=" ")
self.output_label.config(text=" ")
for i in range(len(self.ovals)):
x, y = self.ovals[i]["coords"]
self.canvas.create_oval(x, y, x + 10, y + 10, fill=self.c_color, outline=self.c_color)
if self.cities[self.city_count] == "Le_Havre":
self.canvas.create_text(x, y + 12, fill=self.h_color, font="Calibri 10", text="Le Havre",
tag=self.cities[self.city_count] + "_text")
elif self.cities[self.city_count] == "Le_Mans":
self.canvas.create_text(x, y + 12, fill=self.h_color, font="Calibri 10", text="Le Mans",
tag=self.cities[self.city_count] + "_text")
else:
self.canvas.create_text(x, y + 12, fill=self.h_color, font="Calibri 10",
text=self.cities[self.city_count],
tag=self.cities[self.city_count] + "_text")
self.city_count += 1
for i in range(len(self.lines)):
x1, y1, x2, y2 = self.lines[i]["coords"]
j = random.randint(0, len(self.colors) - 1)
self.canvas.create_line(x1, y1, x2, y2, fill=self.colors[j])
self.canvas.create_text((x1 + x2) / 2, (y1 + y2) / 2, fill=self.colors[j], font="Calibri 10",
text=str(int(distance(x1, y1, x2, y2, 1))))
self.conn_count += 1
(4)下面这些方法扩展了地图编辑和操作功能,具体说明如下所示。
- 方法select_cities的功能是选择源和目标城市,并准备接收用户的点击事件以获取这些信息。
- 方法set_connections的功能是设置连接的数量限制,并显示消息提示框以提醒用户连接数量的范围。
- 方法binder的功能是绑定不同的鼠标点击事件处理程序,以便根据用户选择的操作类型创建城市或连接。
- 方法add_buttons的功能是清除并刷新画布,并向用户显示提示信息,以便他们可以在灰色框内添加城市。
- 方法change_map的功能是根据用户选择的操作类型绑定相应的鼠标点击事件处理程序,允许用户移动城市、删除城市或删除连接。
def select_cities(self):
self.load_same()
self.canvas.unbind("<Button-1>")
self.canvas.bind("<Button-1>", self.dst_and_src)
self.selected_cities = []
msg.showinfo(title="Info", message="Click the source and destination cities.")
def set_connections(self):
c = self.city_total.get()
msg.showinfo(title="Number of Connections", message="Number of connections must be between {} and {}."
.format(c - 1, int(c*(c - 1)/2)))
def binder(self):
self.canvas.unbind("<Button-1>")
self.canvas.bind("<Button-1>", self.create_by_input)
def add_buttons(self):
self.refresh_canvas()
msg.showinfo(title="Info", message="Insert cities inside the gray frame.")
def change_map(self):
self.canvas.unbind("<Button-1>")
self.gonna_move = -1
if self.change_type.get() == 0: # move city
self.canvas.bind("<Button-1>", self.move_city)
elif self.change_type.get() == 1: # delete city
self.canvas.bind("<Button-1>", self.delete_city)
elif self.change_type.get() == 2: # delete connection
self.canvas.bind("<Button-1>", self.delete_connection)
(5)方法find_city用于在给定坐标 (x, y) 下查找是否存在城市,并返回找到的城市的索引。它遍历了保存城市信息的 self.ovals 列表,检查坐标 (x, y) 是否在城市的范围内。如果找到了城市,返回 True 和城市的索引;如果没有找到,返回 False 和 -1。
def find_city(self, x, y):
i = 0
found = False
while i < len(self.ovals) and not found:
x1, y1 = self.ovals[i]["coords"]
if x1 - 1 < x < x1 + 11 and y1 - 1 < y < y1 + 11: # it is a city
found = True
else:
i += 1
return found, i
(6)方法find_connections用于查找与给定城市索引相关联的连接线。它首先确定该城市的坐标 (x, y),然后遍历存储连接线信息的 self.lines 列表。对于每条连接线,它检查是否连接到给定的城市(通过与城市的坐标进行比较)。如果找到连接到该城市的线,则将该连接的索引添加到 self.gonna_move_cons 列表中,并将该连接从画布上删除。
def find_connections(self, index):
x, y = self.ovals[index]["coords"]
for i in range(len(self.lines)):
x1, y1, x2, y2 = self.lines[i]["coords"]
if distance(x1 - 5, y1 - 5, x, y, 0) == 0 or distance(x2 - 5, y2 - 5, x, y, 0) == 0:
# print("Bağlantı var: ", self.lines[i])
self.gonna_move_cons.append(i)
name = "{},{}-{},{}".format(x1, y1, x2, y2)
self.canvas.delete(name + "_line")
self.canvas.delete(name + "_text")
(7)方法move_city实现了移动城市的功能。当用户点击画布上的城市时,该方法会首先确定用户点击的城市。然后,如果该城市被找到,它会将要移动的城市索引设置为找到的城市索引,并删除该城市的当前位置的椭圆形和标签,并查找与该城市相关联的连接线。接下来,当用户再次在画布上点击时,该方法会在新位置创建城市的椭圆形和标签,并更新该城市的坐标信息。同时,它会调整与该城市相关联的连接线的位置以确保其连接到新的城市位置。
def move_city(self, event):
x = event.x
y = event.y
if self.gonna_move == -1: # city
found, index = self.find_city(x, y)
if found:
self.gonna_move = index
self.canvas.delete(self.ovals[self.gonna_move]["name"] + "_oval")
self.canvas.delete(self.ovals[self.gonna_move]["name"] + "_text")
self.find_connections(index)
else: # destination
self.canvas.create_oval(x, y, x + 10, y + 10, fill=self.c_color, outline=self.c_color,tag=self.ovals[self.gonna_move]["name"] + "_oval")
if self.cities[self.gonna_move] == "Le_Havre":
self.canvas.create_text(x, y + 12, fill=self.h_color, font="Calibri 10", text="Le Havre",tag=self.cities[self.gonna_move] + "_text")
elif self.cities[self.gonna_move] == "Le_Mans":
self.canvas.create_text(x, y + 12, fill=self.h_color, font="Calibri 10", text="Le Mans",tag=self.cities[self.gonna_move] + "_text")
else:
self.canvas.create_text(x, y + 12, fill=self.h_color, font="Calibri 10", text=self.cities[self.gonna_move],tag=self.ovals[self.gonna_move]["name"] + "_text")
city = {"coords": (x, y), "name": self.cities[self.gonna_move]}
old_x, old_y = self.ovals[self.gonna_move]["coords"]
self.ovals[self.gonna_move] = city
for i in range(len(self.gonna_move_cons)):
k = self.gonna_move_cons[i]
x1, y1, x2, y2 = self.lines[k]["coords"]
if old_x + 5 == x2 and old_y + 5 == y2:
x2 = x1
y2 = y1
j = random.randint(0, len(self.colors) - 1)
name = "{},{}-{},{}".format(x + 5, y + 5, x2, y2)
self.canvas.create_line(x + 5, y + 5, x2, y2, fill=self.colors[j], tag=name + "_line")
self.canvas.create_text(int((x + x2 + 5) / 2), int((y + y2 + 5) / 2), fill=self.colors[j],
font="Calibri 10", text=str(int(distance(x + 5, y + 5, x2, y2, 1))),
tag=name + "_text")
line = {"from": find_in_ovals(x, y), "to": find_in_ovals(x2 - 5, y2 - 5),
"coords": (x + 5, y + 5, x2, y2)}
self.lines[k] = line
self.gonna_move = -1
self.gonna_move_cons = []
(8)方法delete_city实现了删除城市的功能。当用户点击画布上的城市时,该方法首先会找到用户点击的城市。如果找到了城市,它会删除该城市的椭圆形和标签,并调整城市列表和连接线列表以反映删除的城市。接下来,它会删除与该城市相关的所有连接线,并更新连接线计数。
def delete_city(self, event):
x = event.x
y = event.y
found, index = self.find_city(x, y)
if found:
self.canvas.delete(self.ovals[index]["name"] + "_oval")
if self.ovals[index]["name"] == "Le Havre":
self.canvas.delete("Le_Havre" + "_text")
elif self.ovals[index]["name"] == "Le Mans":
self.canvas.delete("Le_Mans" + "_text")
else:
self.canvas.delete(self.ovals[index]["name"] + "_text")
for i in range(index, self.city_count - 1):
self.cities[i], self.cities[i+1] = self.cities[i+1], self.cities[i]
self.city_count -= 1
self.find_connections(index)
del self.ovals[index]
self.gonna_move_cons.sort(reverse=True)
for i in range(len(self.gonna_move_cons)):
k = self.gonna_move_cons[i]
del self.lines[k]
self.conn_count -= 1
self.gonna_move_cons = []
return
(9)方法delete_connection实现了删除连接线的功能。当用户点击画布上的连接线时,该方法会识别用户点击的对象。如果用户点击的对象是一条连接线,它会从画布上删除该连接线以及与之相关的文本标签,并更新连接线列表以反映删除的连接线。
def delete_connection(self, event):
print("Geldim")
x = event.x
y = event.y
obje = self.canvas.find_closest(x, y)
if len(self.canvas.coords(obje)) == 4 and self.canvas.itemcget(obje, "fill") != "Magenta2": # it's a line
print("It's a line")
x1, y1, x2, y2 = self.canvas.coords(obje)
print(x1, y1, x2, y2)
name = "{},{}-{},{}".format(int(x1), int(y1), int(x2), int(y2))
self.canvas.delete(name + "_line")
self.canvas.delete(name + "_text")
i = 0
found = False
while i < (len(self.lines)) and not found:
x3, y3, x4, y4 = self.lines[i]["coords"]
if distance(x1, y1, x3, y3, 0) == 0 and distance(x2, y2, x4, y4, 0) == 0:
del self.lines[i]
print("Deleted.")
found = True
self.conn_count -= 1
else:
i += 1
return
(10)方法overlap用于检测新添加的城市是否与现有的城市重叠。它通过迭代现有城市的坐标,并计算新城市与每个现有城市之间的距离,来判断是否发生重叠。如果两个城市之间的距离小于30个像素,则认为发生了重叠,并返回True;否则返回False。
def overlap(self, x, y):
overlap = False
i = 0
while i < len(self.ovals) and not overlap:
x1, y1 = self.ovals[i]["coords"]
if distance(x, y, x1, y1, 1) < 30:
overlap = True
i += 1
return overlap