Introduction
This is related to an earlier post which was on panning and scaling a node graph visualized with Tkinter. In this post, the goal is to implement a level preview function where when the user hovers over a node they can see the associated file in a rectangle that appears. When they stop hovering over the node, the preview should go away.
Additionally, there is a section on writing an update_node
function that simplified the codebase.
Implementing Preview File
The gif above is the final product. It works by creating a frame and a label. The frame is where the label will be placed. The label is where the text goes.
self.preview_frame = tk.Frame(self.canvas)
self.preview_frame.place(x=-1000, y=-1000) # off screen
self.preview_label = tk.Label(
self.preview_frame,
width=32,
height=17,
font="TkFixedFont"
)
self.preview_label.pack()
- Preview frame is placed offscreen at
(-1000, -1000)
. It was recommended to usepack
to make things appear andpack_forget
to make them disappear. When I tried this, the window was resized to the size of the frame. Rather than waste a day figuring out exactly how it should be done in Tkinter, I decided to initialize the frame offscreen and place it onscreen when relevant. - Width and height are hardcoded because I was lazy. They could be calculated based the string that is to be placed inside.
TkFixedFont
is a default monospace font. Monospace is important because the preview is for levels, and columns need to be aligned for a better view.
def on_enter(event):
self.preview_label.config(text=N.level)
self.preview_frame.place(
x=(N.x + NODE_WIDTH + 1) * self.scale,
y=N.y*self.scale
)
def on_exit(event):
self.preview_frame.place(x=-1000, y=-1000)
frame.bind('<Enter>', on_enter)
frame.bind('<Leave>', on_exit)
The code above is not exactly what you will find in the codebase, but this is a the basic idea. Tkinter has events <Enter>
and <Leave>
. So, all we have to do is bind a function to each event. When the user’s mouse enters the frame, we set the preview label to the text of the level, which is a member of node N
. Then, the frame is moved based on node N
’s position. However, note that this has to be scaled, else the preview window will be far away from where it should be. When the user’s cursor leaves the frame, the preview_frame
is placed offscreen again.
Update Node
In my last post, I pointed out that the functions for moving a node and scaling one were very similar, and that it would be smart to make one function to handle both. I also said that I wasn’t going to do it because I wasn’t a perfectionist, and the code was only for me. While both of those facts remain true, I found myself implementing the preview function, and I wanted to clean up the code. So, I combined the two functions into one: update_node
.
def update_node(self, n: CustomNode, dx: float, dy: float):
## Update rectangle placement
self.canvas.move(n.rect_id, dx, dy)
self.canvas.itemconfig(n.rect_id, tags=("rect", "dragged"))
x1, y1, _x2, _y2 = self.canvas.coords(n.rect_id)
n.x += dx
n.y += dy
# rectangle
self.canvas.coords(
n.rect_id,
n.x * self.scale,
n.y * self.scale,
(n.x + NODE_WIDTH) * self.scale,
(n.y + NODE_HEIGHT) * self.scale
)
# frame
n.frame.place(
x = (n.x + 1) * self.scale,
y = (n.y + 1) * self.scale
)
# entry
n.entry.config(width=ceil(3*self.scale))
## Update Edge coordinates
# incoming
for tgt in n.neighbors:
line_id = self.G.get_edge(n.name, tgt).line_id
coords = self.canvas.coords(line_id)
self.canvas.coords(
line_id,
(n.x + NODE_WIDTH) * self.scale,
(n.y + NODE_HEIGHT / 2) * self.scale,
coords[2] ,
coords[3]
)
# outgoing
for edge in self.G.incoming_edges(n.name):
coords = self.canvas.coords(edge.line_id)
self.canvas.coords(
edge.line_id,
coords[0],
coords[1],
n.x * self.scale,
(n.y + NODE_HEIGHT / 2) * self.scale
)