Matplotlib 重叠艺术家选择事件顺序
我遇到了一个很奇怪的问题,跟matplotlib的选择事件有关。我有两个可选择的对象,它们一开始是不会重叠的,一个是“孔”,一个是“钉子”。当我选择其中一个时,在处理事件的过程中,我会把另一个对象移动到我刚点击的位置(把“钉子”放进“孔”里)。然后,什么都不做的情况下,移动后的对象(钉子)会生成一个选择事件,尽管在第一次事件生成时它并不在那个位置。我唯一能想到的解释是,事件管理器在处理事件时,仍然在遍历对象的层级,因此在钉子移动到鼠标光标下时,触发了第二个对象的事件。
所以我的问题是——选择事件(或者说任何事件)是如何在画布上遍历重叠的对象的?有没有办法控制这个过程?我觉得如果事件总是从上到下处理(而不是从下到上或随机处理),我就能得到我想要的效果。我找不到足够的文档,长时间在StackOverflow上搜索也没有找到这个具体的问题。下面是一个工作示例,展示了这个问题,使用PathCollections
从scatter
作为钉子和孔:
import matplotlib.pyplot as plt
import sys
class peg_tester():
def __init__(self):
self.fig = plt.figure(figsize=(3,1))
self.ax = self.fig.add_axes([0,0,1,1])
self.ax.set_xlim([-0.5,2.5])
self.ax.set_ylim([-0.25,0.25])
self.ax.text(-0.4, 0.15, 'One click on the hole, and I get 2 events not 1',
fontsize=8)
self.holes = self.ax.scatter([1], [0], color='black', picker=0)
self.pegs = self.ax.scatter([0], [0], s=100, facecolor='#dd8800',
edgecolor='black', picker=0)
self.fig.canvas.mpl_connect('pick_event', self.handler)
plt.show()
def handler(self, event):
if event.artist is self.holes:
# If I get a hole event, then move a peg (to that hole) ...
# but then I get a peg event also with no extra clicks!
offs = self.pegs.get_offsets()
offs[0,:] = [1,0] # Moves left peg to the middle
self.pegs.set_offsets(offs)
self.fig.canvas.draw()
print 'picked a hole, moving left peg to center'
elif event.artist is self.pegs:
print 'picked a peg'
sys.stdout.flush() # Necessary when in ipython qtconsole
if __name__ == "__main__":
pt = peg_tester()
我尝试过设置zorder,让钉子总是在孔的上面,但这并没有改变选择事件的生成方式,特别是这个奇怪的幻影事件。
编辑: 这个问题的背景是我在实现一个钉子单人游戏,所以我想能够捡起一个钉子,然后点击一个空的孔把它放进去。目前,钉子一放下就会立刻被再次选择。
1 个回答
关于时间的问题
你遇到的问题是因为调用pick_event函数的方式。matplotlib会检查每个图形元素,如果被选中,就会立即调用你的处理函数。检查的顺序取决于你定义孔和钉子的顺序(你可以通过改变钉子和孔的定义顺序来验证这一点)。
所以,有一种看起来不错的方法来避免这个问题,就是使用matplotlib提供的定时器。第一次发生选择事件时,你只需将图形元素添加到一个队列中,然后每隔x毫秒处理这些新数据。
下面是这种实现方式的一个例子:
import matplotlib.pyplot as plt
import sys
class peg_tester():
def __init__(self):
self.fig = plt.figure(figsize=(3,1))
self.ax = self.fig.add_axes([0,0,1,1])
self.ax.set_xlim([-0.5,2.5])
self.ax.set_ylim([-0.25,0.25])
self.ax.text(-0.4, 0.15,
'One click on the hole, and I get 2 events not 1',
fontsize=8)
self.holes = self.ax.scatter([1], [0], color='black', picker=0)
self.pegs = self.ax.scatter([0], [0], s=100, facecolor='#dd8800',
edgecolor='black', picker=0)
self.fig.canvas.mpl_connect('pick_event', self.handler)
# Creating a timer with a interval of 100 ms
self.timer=self.fig.canvas.new_timer(interval=100)
# Queue of pegs and holes that have been picked and waiting to be processed
self.list_pegs_holes=[]
self.timer.add_callback(self.update_pos,self.list_pegs_holes)
self.timer.start()
plt.show()
# Add the artist to the queue of artists awaiting to be processed
def handler(self, event):
self.list_pegs_holes.append(event.artist)
# Management of the queue
def update_pos(self,list_pegs_holes):
while len(list_pegs_holes)!=0:
artist=list_pegs_holes.pop(0)
if artist is self.holes:
# If I get a hole event, then move a peg (to that hole) ...
# but then I get a peg event also with no extra clicks!
offs = self.pegs.get_offsets()
offs[0,:] = [1,0] # Moves left peg to the middle
self.pegs.set_offsets(offs)
self.fig.canvas.draw()
print 'picked a hole, moving left peg to center'
elif artist is self.pegs:
print 'picked a peg'
sys.stdout.flush() # Necessary when in ipython qtconsole
if __name__ == "__main__":
pt = peg_tester()
大部分代码是你提供的,我只是添加了定时器的实现。
不是最优的(过时的)
通过为你的图形元素定义特定的选择器,可以修复这种行为。不过,这样的话,你就无法在同一个位置选择多个项目了。
看看这个例子,它解决了在不该选择钉子时仍然选择的问题:
-将钉子的定义替换为:
self.pegs = self.ax.scatter([0], [0], s=100, facecolor='#dd8800',edgecolor='black', picker=self.pegs_picker)
-添加函数pegs_picker:
def pegs_picker(figure,pegs,event):
# Check that the pointer is not on holes
if figure.holes.contains(event)[0]:
return False, dict()
else:
return True, dict()
这样,钉子只有在没有与孔重叠时才能被选择。
我认为这可能是实现你想要的行为的方式,但由于我不太清楚你具体想要什么,所以无法提供更精细的选择函数。