Python Tkinter中如何排除find_closest匹配的线条对象
Tkinter 排除find_closest匹配范围内指定画布对象的实现方案
问题描述
- 基于Tkinter Canvas开发六边形网格路径记录工具,通过监听鼠标左键点击事件,在相邻点击点之间绘制连线标识移动路径。
- 现有故障:绘制的路径连线属于独立画布对象,默认会被
find_closest()方法纳入匹配范围。由于连线绘制在六边形上层,随着连线数量增加,下层六边形几乎无法被选中,网格交互完全失效。 - 最小复现代码如下:
import tkinter from tkinter import * from math import radians, cos, sin, sqrt class App: def __init__(self, parent): self.parent = parent self.c1 = Canvas(self.parent, width=int(1.5*340), height=int(1.5*270), bg='white') self.c1.grid(column=0, row=0, sticky='nsew') self.clickcount = 0 self.clicks = [(0,0)] self.startx = int(20*1.5) self.starty = int(20*1.5) self.radius = int(20*1.5) # 六边形边长 self.hexagons = [] self.columns = 10 self.initGrid(self.startx, self.starty, self.radius, self.columns) self.c1.bind("<Button-1>", self.click) def initGrid(self, x, y, radius, cols): """ 绘制二维六边形网格 """ radius = radius column = 0 for j in range(cols): startx = x starty = y for i in range(6): breadth = column * (1.5 * radius) if column % 2 == 0: offset = 0 else: offset = radius * sqrt(3) / 2 self.draw(startx + breadth, starty + offset, radius) starty = starty + 2 * (radius * sqrt(3) / 2) column = column + 1 def draw(self, x, y, radius): start_x = x start_y = y angle = 60 coords = [] for i in range(6): end_x = start_x + radius * cos(radians(angle * i)) end_y = start_y + radius * sin(radians(angle * i)) coords.append([start_x, start_y]) start_x = end_x start_y = end_y hex = self.c1.create_polygon(coords[0][0], coords[0][1], coords[1][0], coords[1][1], coords[2][0], coords[2][1], coords[3][0], coords[3][1], coords[4][0], coords[4][1], coords[5][0], coords[5][1], fill='black') self.hexagons.append(hex) def click(self, evt): self.clickcount = self.clickcount + 1 x, y = evt.x, evt.y tuple_alfa = (evt.x, evt.y) self.clicks.append(tuple_alfa) if self.clickcount >= 2: start = self.clicks[self.clickcount - 1] startx = start[0] starty = start[1] self.c1.create_line(evt.x, evt.y, startx, starty, fill='white') clicked = self.c1.find_closest(x, y)[0] print(clicked) root = tkinter.Tk() App(root) root.mainloop()
注:原代码连线逻辑存在索引bug,取点时使用了当前点击点作为连线起点/终点,会绘制零长度无效线条,修复方案见下文修改代码。
实现思路
Tkinter Canvas没有提供直接给find_closest()传排除名单的参数,最稳定高效的方案是利用标签系统+局部范围搜索实现过滤:
- 给不同类型的画布对象打独立标签做区分,六边形统一打
hex标签,路径连线统一打path标签 - 放弃全画布范围的
find_closest()调用,改为搜索点击点周围3像素范围内的所有对象(范围可根据点击精度调整) - 遍历搜索结果,跳过连线类对象,第一个匹配到的六边形就是目标选中对象,从根源上避免连线干扰选中逻辑
修改后可运行代码
仅需修改3处逻辑即可解决问题:
- 创建六边形时添加
hex标签 - 创建连线时添加
path标签,同时修复连线索引bug - 替换选中逻辑为局部搜索+标签过滤
import tkinter from tkinter import * from math import radians, cos, sin, sqrt class App: def __init__(self, parent): self.parent = parent self.c1 = Canvas(self.parent, width=int(1.5*340), height=int(1.5*270), bg='white') self.c1.grid(column=0, row=0, sticky='nsew') self.clickcount = 0 self.clicks = [(0,0)] self.startx = int(20*1.5) self.starty = int(20*1.5) self.radius = int(20*1.5) # 六边形边长 self.hexagons = [] self.columns = 10 self.initGrid(self.startx, self.starty, self.radius, self.columns) self.c1.bind("<Button-1>", self.click) def initGrid(self, x, y, radius, cols): """ 绘制二维六边形网格 """ radius = radius column = 0 for j in range(cols): startx = x starty = y for i in range(6): breadth = column * (1.5 * radius) if column % 2 == 0: offset = 0 else: offset = radius * sqrt(3) / 2 self.draw(startx + breadth, starty + offset, radius) starty = starty + 2 * (radius * sqrt(3) / 2) column = column + 1 def draw(self, x, y, radius): start_x = x start_y = y angle = 60 coords = [] for i in range(6): end_x = start_x + radius * cos(radians(angle * i)) end_y = start_y + radius * sin(radians(angle * i)) coords.append([start_x, start_y]) start_x = end_x start_y = end_y # 给六边形添加统一标签hex hex_item = self.c1.create_polygon( coords[0][0], coords[0][1], coords[1][0], coords[1][1], coords[2][0], coords[2][1], coords[3][0], coords[3][1], coords[4][0], coords[4][1], coords[5][0], coords[5][1], fill='black', tags='hex' ) self.hexagons.append(hex_item) def click(self, evt): self.clickcount = self.clickcount + 1 x, y = evt.x, evt.y current_click = (evt.x, evt.y) self.clicks.append(current_click) if self.clickcount >= 2: # 修复连线索引bug,取上一个点击点作为连线起点,给连线加path标签 last_click = self.clicks[self.clickcount - 2] self.c1.create_line( last_click[0], last_click[1], x, y, fill='white', width=2, tags='path' ) # 局部搜索+标签过滤,跳过连线只选六边形 nearby_items = self.c1.find_overlapping(x-3, y-3, x+3, y+3) clicked = None for item in nearby_items: if 'hex' in self.c1.gettags(item): clicked = item break print(clicked) root = tkinter.Tk() App(root) root.mainloop()
补充说明
- 局部搜索的范围(当前设置为3像素)可以根据实际需求调整,数值越大点击容错越高,数值越小选中精度越高
- 该方案性能远高于遍历所有六边形计算距离的实现,即使网格规模很大、连线数量很多,选中操作也不会有卡顿
- 如果后续需要新增其他不参与选中的画布元素(比如路径标注文字),只需要给对应元素打独立标签,在过滤逻辑里跳过即可,扩展性很强
内容的提问来源于stack exchange,提问作者Bamischijf
相关产品推荐
相关产品推荐

