GTK3 &Python interactive - architecture of classes - PopupMenu Of GTK3 on matplotlib Canvas - python

I am using Python & GTK3 under mac OS X Sierra in order to display a database of nodes as a Diagram. It is a MVP architecture.
I use matplotlib to display my figure. As my display is interactive so I have to receive signals. I decided to receive signals directly from my matplotlib canvas instead using GTK GUI, as GTK event do not have enough parameters. Actually I want to receive signals on my canvas using matplotlib and show a popup menu of GTK.
The function that you have to look is MainFigurePressed. The idea is I received signals on my canvas with the program DrawChronoMap using the function Detecte() than I popup a menu of my GTK GUI calling View by connecting Viewhandlers. In order to launch my program, my controller will call my view.I am wondering how can I pass in argument my class Viewhandler in my class DrawChronoMap?
How should I do? How should I change my architecture to do that?
I have three classes actually :
class View():
def __init__(self, MainController):
#set windows
self.window = Gtk.Window()
self.window.connect("delete-event", Gtk.main_quit)
self.window.set_default_size(10000, 10000)
self.window.set_title('ChronoMap')
#Init Glade file # Get windows from glade
self.interface = Gtk.Builder()
self.interface.add_from_file("interface1.glade")
self.mainWindow = self.interface.get_object("mainWindow")
self.aboutchronomap = self.interface.get_object("aboutchronomap")
self.fichierdialogue=self.interface.get_object("fichierdialogue")
self.sw=self.interface.get_object("mainFigure")
self.toolbar=self.interface.get_object("MatplotlibToolbar")
self.sw3=self.interface.get_object("scrolledwindow1")
self.sw4=self.interface.get_object("scrolledwindow2")
self.add_node_window=self.interface.get_object("add_node_window")
self.add_edge_window=self.interface.get_object("add_edge_window")
self.modify_edge_window=self.interface.get_object("modify_edge_window")
self.modify_node_window=self.interface.get_object("modify_node_window")
self.add_reference_node_edge=self.interface.get_object("add_reference_node_edge")
self.popupmenuCartoNode=self.interface.get_object("popupmenuCartoNode")
self.popupmenuCartoEdge=self.interface.get_object("popupmenuCartoEdge")
self.popupmenuCartoOtherplace=self.interface.get_object("popupmenuCartoOtherplace")
self.popupmenuChronoNode=self.interface.get_object("popupmenuChronoNode")
self.popupmenuChronoZoneBC=self.interface.get_object("popupmenuChronoZoneBC")
self.popupmenuChronoCursor=self.interface.get_object("popupmenuChronoCursor")
#Global controller
self.controller=MainController
#Init CartoCanvas
self.figCarto = Figure(figsize=(20,20), dpi=80)
self.axCarto = self.figCarto.add_subplot(111)
self.canvasCarto = FigureCanvas(self.figCarto)
# Init ChronoCanvas
self.figChrono = Figure(figsize=(20,20), dpi=80)
self.axChrono = self.figChrono.add_subplot(111)
self.canvasChrono = FigureCanvas(self.figChrono)
#Create a New graph on the controller
self.controller.create_new_graph("CartoChronomap")
#add node & edges
nodeA=self.controller.create_evenement("outdated research material", "01-01-2016 00:00:00", "01-02-2016 00:00:00", 1, "BLAH BLAH BLAH", "http://")
nodeB= self.controller.create_evenement("Projected tsunami frequency too low", "08-08-2016 00:00:00", "09-10-2016 00:00:00", 1, "OK", "http://")
nodeC=self.controller.create_evenement("EV C", "08-07-2016 00:00:00", "09-08-2016 00:00:00", 1, "HOOOOO", "http://")
nodeD=self.controller.create_evenement("Accident", "08-10-2016 00:00:00", "09-11-2016 00:00:00", 1, "HOOOOO", "http://")
self.controller.create_edge(nodeA,nodeB, "LeLien", "Une mega explosion", "[]")
self.controller.create_edge(nodeB,nodeA, "InverseLien", "Une giga explosion", "[]")
self.controller.create_edge(nodeC,nodeD, "LienTest", "Ceci est un lien test", "[]")
self.controller.calculate_position('spring_layout');
#Connect to draw chronograph
self.FdessinChrono=Draw_chrono.DrawChronoMap(self.axChrono,self.controller)
#Connect to draw Cartograph
self.FdessinCarto = Draw_cartograph.DrawCartoMap(self.axCarto, self.controller)
#draw
self.FdessinCarto.draw_cartograph()
self.FdessinChrono.draw_chronograph()
#MouseFunction Carto
self.FdessinCarto.zoom_wheel()
self.FdessinCarto.pan_drag()
#self.FdessinCarto.drag_node()
self.FdessinCarto.ChangeNodeColor()
self.FdessinCarto.node_popup_mouse_over()
self.FdessinCarto.edge_popup_mouse_over()
#Global carto event & chrono event
self.CartoEvent = self.FdessinCarto.Detect()
self.ChronoEvent = self.FdessinChrono.Detect()
print(self.CartoEvent,self.ChronoEvent)
#MouseFunction Chrono
self.FdessinChrono.cursor()
self.FdessinChrono.ChangeColor()
#self.FdessinChrono.pan_drag()
self.FdessinChrono.node_popup_mouse_over()
#Display Mode
self.display_Mode = None
#Creating the ListStore model
#node_liststore
self.node_liststore = Gtk.ListStore(str, str, str,str,str,str)
if len(self.FdessinCarto.pos) != 0:
for i,node in enumerate(self.FdessinCarto.pos):
self.node_liststore.append([str(node.title),str(node.start_time),str(node.end_time),str(node.node_group),str(node.description),str(node.attachment_list)])
#edge_liststore
self.edge_liststore = Gtk.ListStore(str, str, str,str,str)
if len(self.FdessinCarto.edgelist) !=0:
edge_prop=self.FdessinCarto.controller.edge_data(nodeA,nodeB)
edge_prop1=self.FdessinCarto.controller.edge_data(nodeB,nodeA)
self.edge_liststore.append([edge_prop['label'],str(nodeA.title),str(nodeB.title),edge_prop['description'],edge_prop['attachment_list']])
self.edge_liststore.append([edge_prop1['label'],str(nodeA.title),str(nodeB.title),edge_prop1['description'],edge_prop1['attachment_list']])
#creating the filtre
self.node_filter = self.node_liststore.filter_new()
self.edge_filter = self.edge_liststore.filter_new()
#setting the filter function, note that we're not using the
self.node_filter.set_visible_func(ViewHandler.node_filter_func)
self.edge_filter.set_visible_func(ViewHandler.edge_filter_func)
#creating the treeview for Node, making it use the filter as a model, and adding the columns
self.treeviewNode = Gtk.TreeView.new_with_model(self.node_liststore)
for i, column_title in enumerate(["Nom", "Date début", "Date fin", "Type de noeud", "Description du noeud","fichier"]):
self.Noderenderer = Gtk.CellRendererText()
self.Noderenderer.set_property("editable", True)
column = Gtk.TreeViewColumn(column_title, self.Noderenderer, text=i)
self.treeviewNode.append_column(column)
#self.Noderenderer.connect("edited", self.onButtonCreateNode)
#creating the treeview for edge
self.treeviewEdge = Gtk.TreeView.new_with_model(self.edge_liststore)
for i, column_title in enumerate(["Nom", "Noeud 1", "Noeud 2", "Description du lien","fichier"]):
self.Edgerenderer = Gtk.CellRendererText()
self.Edgerenderer.set_property("editable", True)
column = Gtk.TreeViewColumn(column_title, self.Edgerenderer, text=i)
self.treeviewEdge.append_column(column)
# Connect with signals
self.interface.connect_signals(ViewHandler(self))
#setting up the layout, putting the treeview in a scrollwindow
self.sw3.add(self.treeviewNode)
self.sw4.add(self.treeviewEdge)
self.sw3.show_all()
self.sw4.show_all()
self.window.add(self.sw)
self.sw.show_all()
# All ready - open interface
Gtk.main()
def update_data(self):
self.CartoEvent = self.FdessinCarto.Detect()
self.ChronoEvent = self.FdessinChrono.Detect()
print(self.CartoEvent,self.ChronoEvent)
class ViewHandler():
def __init__(self, ViewConnection):
self.View = ViewConnection
def resetplot(self):
axCarto.cla()
axCarto.set_xlim(0,10)
axCarto.set_ylim(0,10)
axCarto.grid(True)
axChrono.cla()
axChrono.set_xlim(0,10)
axChrono.set_ylim(0,10)
axChrono.grid(True)
# All button signals of GTK
#Signal to open windows "creation of node"
def create_node_button_press_event(self,widget):
self.View.add_node_window.show_all()
#Signal to open window "creation of link"
def create_link_button_press_event(self,widget):
self.View.add_edge_window.show_all()
def onButtonCreateNode(self,widget):
self.resetplot()
nom=self.View.interface.get_object('name_node1').get_text()
node_type=self.View.interface.get_object('node_type1').get_text()
start_time_node=self.View.interface.get_object('start_time_node1').get_text()
end_time_node=self.View.interface.get_object('end_time_node1').get_text()
#print(nom,node_type,start_time_node,end_time_node)
self.View.node_liststore.append([nom, start_time_node, end_time_node, node_type, "BLAH BLAH BLAH", "http://"])
self.View.FdessinCarto.controller.create_evenement(nom, start_time_node, end_time_node, node_type, "BLAH BLAH BLAH", "http://")
self.View.FdessinChrono.controller.create_evenement(nom, start_time_node, end_time_node, node_type, "BLAH BLAH BLAH", "http://")
self.View.canvasCarto.draw()
self.View.canvasChrono.draw()
self.View.add_node_window.destroy()
self.View.sw.show_all()
self.View.sw3.show_all()
def onButtonAddFileEdge(self,widget):
pass
def onButtonCreateEdge(self,widget):
nom=self.View.interface.get_object('name_edge2').get_text()
edge_description=self.View.interface.get_object('edge_type2').get_text()
node1_edge=self.View.interface.get_object('node1_edge_entry').get_text()
node2_edge=self.View.interface.get_object('node2_edge_entry2').get_text()
#create signal with liststore
self.View.edge_liststore.append([node1_edge,node2_edge, nom, edge_description, "[]"])
#create link with canvas
self.View.FdessinCarto.controller.create_edge(node1_edge,node2_edge, nom, edge_description, "[]")
self.View.FdessinChrono.controller.create_edge(node1_edge,node2_edge, nom, edge_description, "[]")
self.View.canvasCarto.draw()
self.View.canvasChrono.draw()
#register it in the treeStore
edge_prop=list(self.View.FdessinCarto.controller.edge_data(node1_edge,node2_edge))
self.View.sw.show_all()
self.View.add_edge_window.destroy()
#Signal to contextual menu
def onMainFigurePressed(self,widget,event):
print(event.type, event.button, event.window, event.x, event.y, event.time,event.get_state(),event.time)
#update event
self.View.update_data()
self.CartoEvent = self.View.FdessinCarto.Detect()
self.ChronoEvent = self.View.FdessinChrono.Detect()
print(self.View.CartoEvent,self.View.ChronoEvent,self.CartoEvent,self.ChronoEvent)
if event.type == Gdk.EventType.ENTER_NOTIFY:
print("yes, enter")
if event.type == Gdk.EventType.LEAVE_NOTIFY:
print("No, out")
if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:
if self.display_Mode == "carto" :
print("oui, carto " )
self.View.popupmenuCartoNode.popup(None, None, None, None, event.button, event.time)
elif self.display_Mode == "chrono" :
print( "oui chrono")
self.View.popupmenuChronoNode.popup(None, None, None, None, event.button, event.time)
else:
return None
def onButtonModifyNode(self,widget,event):
#print("modify windows show all")
self.View.modify_node_window.show_all()
def onButtonDeleteNode(self,widget,event):
#print("button pressed delete")
self.View.FdessinCarto.deleteNode()
def onButtonLinkNode(self,widget,event):
#print("hello")
self.View.add_edge_window.show_all()
def onButtonCopyNode(self,widget,event):
#print("copy")
self.View.FdessinCarto.copynode()
#print("copy it")
def onButtonOtherplaceCreateNode(self,widget,event):
self.View.add_node_window.show_all()
def onButtonAttributsNode(self,widget,event):
self.View.FdessinCarto.display_node_attributs()
#Signal of menubars
def on_node_file_button_press_event(self,widget):
self.View.add_node_window.show_all()
def on_create_edge_button_press_event(self,widget):
self.View.add_edge_window.show_all()
def on_Open_button_press_event(self,widget,event):
self.View.fichierdialogue.show_all()
#signal of about
def on_gtk_about_button_release_event(self,widget,event):
self.View.aboutchronomap.show_all()
# close window
def on_close_button_press_event(self,widget,event):
self.View.on_quit_button_press_event (widget,event)
def on_quit_button_press_event (self,widget,event):
#pop up menu
dialog = Gtk.MessageDialog(None, 0, Gtk.MessageType.INFO,Gtk.ButtonsType.OK_CANCEL, "Vous partez?")
dialog.format_secondary_text("Voulez vous toujours partir?")
response=dialog.run()
if response == Gtk.ResponseType.OK:
Gtk.main_quit()
elif response == Gtk.ResponseType.CANCEL:
dialog.destroy()
dialog.destroy()
return True
def on_confirmation_deletenode_button_press_event (self,widget,event):
#pop up menu
dialog = Gtk.MessageDialog(None, 0, Gtk.MessageType.INFO,Gtk.ButtonsType.OK_CANCEL, "Suppression du noeud?")
dialog.format_secondary_text("Voulez vous vraiment supprimer le noeud?")
response=dialog.run()
if response == Gtk.ResponseType.OK:
Gtk.main_quit()
elif response == Gtk.ResponseType.CANCEL:
dialog.destroy()
dialog.destroy()
return True
def on_mainWindow_destroy(self, widget):
Gtk.main_quit()
def on_carto_display_button_press_event(self,widget,event):
self.display_Mode = "carto"
child=self.View.sw.get_child()
child1 = self.View.toolbar.get_child()
#print(child)
if child != None:
self.View.toolbar.remove(child1)
self.View.sw.remove(child)
self.box.remove(self.View.canvasChrono)
self.box=Gtk.Box()
self.View.sw.add(self.box)
self.box.pack_start(self.View.canvasCarto, True, True, 0)
#Add toolbar
toolbar = NavigationToolbar(self.View.canvasCarto, self.View.window)
self.View.toolbar.add_with_viewport(toolbar)
self.View.sw.show_all()
def on_chrono_display_button_press_event(self,widget,event):
self.display_Mode= "chrono"
child = self.View.sw.get_child()
child1 = self.View.toolbar.get_child()
if child != None:
self.View.toolbar.remove(child1)
self.View.sw.remove(child)
self.box.remove(self.View.canvasCarto)
self.View.FdessinChrono.draw_chronograph()
self.box=Gtk.Box()
self.View.sw.add(self.box)
self.box.pack_start(self.View.canvasChrono, True, True, 0)
#Add toolbar
toolbar = NavigationToolbar(self.View.canvasChrono, self.View.window)
self.View.toolbar.add_with_viewport(toolbar)
self.View.sw.show_all()
class DrawChronoMap:
def __init__(self,ax, controller):
#Global controller
self.controller = controller
#Global graph
self.G = self.controller.total_graph()
#Global model
self.model=self.controller.model
#Global Axis
self.ax = ax
#Global figure
self.fig = self.ax.get_figure()
#Gloal canvas
self.canvas = self.ax.get_figure().canvas
#Global list
self.nodelist = self.controller.get_node_list()
self.edgelist=self.controller.get_edge_list()
#Global empty collection
#Global nodecollection
self.nodecollection=None
#Gloabl datanode
self.datanode = []
#Global empty list
self.eventnode_with_rectangle=[]
self.start_date=[]
self.end_date=[]
self.event_name=[]
#Global data axes
self.axis_x=[]
#Global label axis y
self.yticks = None
# Drag time
self.drag_time=0
self.press = []
self.drag_press = []
self.xdrag=0
self.ydrag=0
#event data
self.xdata=0
self.ydata=0
#event if we selecte edge
self.node1=None
self.node2=None
#cursor
self.ly = self.ax.axvline(color='k') # the vert line
self.txt = self.ax.text(0.7, 0.9, '', transform=self.ax.transAxes)
#Node attibute popup
self.popup = self.ax.text(0, 0, '', style='italic',bbox = {'facecolor':'y', 'alpha':0.5, 'pad':10})
#Edge attribute popup
self.popupedge = self.ax.text(0, 0, '', style='italic',bbox = {'facecolor':'y', 'alpha':0.5, 'pad':10})
def draw_chronograph(self):
#update graph
self.G = self.controller.total_graph()
#update data of nodecollection
self.nodelist = self.controller.get_node_list()
for i in range(len(self.nodelist)):
self.event_name.append(self.nodelist[i].title)
bottom = ((i-1)*0.5) + 1.0
width = self.nodelist[i].end_time - self.nodelist[i].start_time
left=self.nodelist[i].start_time
height=0.3
rectangle = self.ax.bar(left,height,width,bottom)
rectangle.bottom = bottom
rectangle.i = i
self.eventnode_with_rectangle.append([self.nodelist[i],rectangle])
self.nodelist[i].pos=i
self.datanode.append(self.nodelist[i].start_time)
self.datanode.append(self.nodelist[i].end_time)
#pos of i in the dictionnary
taille=len(self.event_name)
pos=arange(0.5,(taille+2)*0.5+0.5,0.5)
self.yticks=yticks(pos,self.event_name)
locsy,labelsy=self.yticks
self.ax.set_yticks(pos)
self.ax.set_yticklabels(labelsy, size='small')
self.ax.axis('tight')
self.ax.set_ylim(0, taille*0.5+0.5)
self.ax.grid(color = 'g', linestyle = ':')
font = font_manager.FontProperties(size='small')
self.ax.legend(loc=1,prop=font)
#format the x-axis
self.ax.set_xlim(min(self.datanode), max(self.datanode))
self.ax.xaxis.tick_top()
# Finish up
self.ax.invert_yaxis()
self.fig.autofmt_xdate()
#init cursor
self.ly.set_xdata((min(self.datanode)+ max(self.datanode))/2)
self.txt.set_text('y=%s' % ((min(self.datanode)+ max(self.datanode))/2))
self.canvas.draw()
def ChangeColor(self):
def on_press(event):
#update self.press
self.press=[]
x=event.xdata
y=event.ydata
self.press=[x,y]
#print(event.button)
if event.button == 1:
for i,rectangle in self.eventnode_with_rectangle:
if (i.start_time< self.press[0] <i.end_time) and ((i.pos-1)*0.5+1-0.15< self.press[1] <(i.pos-1)*0.5+1+0.15) :
rectangle=self.ax.barh(((i.pos-1)*0.5)+1.0, i.end_time - i.start_time, left=i.start_time, height=0.3, align='center', color='red', alpha = 0.75)
rectangle=self.ax.barh(((i.pos-1)*0.5)+1.0, i.end_time - i.start_time, left=i.start_time, height=0.3, align='center', color='blue', alpha = 0.75)
self.canvas.draw()
else :
return None
def on_release(event):
self.press = []
self.canvas.draw()
self.canvas.mpl_connect('button_press_event', on_press)
self.canvas.mpl_connect('button_release_event', on_release)
def Detect(self):
def event(event):
actual_node = None
x=event.xdata
y=event.ydata
for i,rectangle in self.eventnode_with_rectangle:
if (i.start_time<x<i.end_time) and ((i.pos-1)*0.5+1-0.15<y<(i.pos-1)*0.5+1+0.15) :
actual_node = i
break
print("function drawchrono %s" %actual_node)
return actual_node
self.canvas.mpl_connect('button_press_event', event)
import BasicModel as md
import View as vw
import networkx as nx
import matplotlib.pyplot as plt
import forceatlas.forceatlas as nxfa2
import undo
import re
import copy
class Controller:
def __init__(self, model):
"""
Initialization: Gets the model
:param model: Model class to use
"""
# Loads the model
self.model = model
if __name__ == '__main__':
# Init Model
MainModel = md.Model()
# Init Controller
MainController = Controller(MainModel)
# Init View
MainView = vw.View(MainController)
# Program Running.

I am going to add some pictures in order to better explain my problem.
Canvas embedded in my GTK environment
This is my GTK environment with the canvas (in white) using matplotlib to display things. My canvas is displayed with matplotlib. The canvas receives mouse events and it wil change the display of the canvas.
Display of the popup menu
We can see a popup menu here. So what is tricky here, it is NO LONGER my canvas which receives the event, but it is actually my GTK environment.
So the problem is here :
I want to display different menu depending on the type of object that I click on. I can do it easily if it is the canvas which receive event as I can directly calculate if the event is on a node or an edge, but if it is a GTK event, I can no longer detect if the event is on a node or on an edge.
So what should I do? should I build a new class in order to connect GTK event and canvas event?
Thanks

Related

How do I use keyboard shortcuts with Gtk StackSwitcher?

What I want to do is make a shortcut keyboard key to switch between Page 1 and Page 2. For instance, pressing Ctrl+S would take me to Page 1 if I am not already there and likewise pressing Ctrl+R would take me to Page 2. I searched the documentation but I couldn't find anything related to what I need. Is there a way to implement it? Please see the below image:
Stack Switcher
Here's the snippet:
class App(Gtk.Application):
def __init__(self, *args, **kwargs):
super(App, self).__init__(*args, **kwargs)
self.connect('activate', self.on_activate)
self.send_stack = None
self.receive_stack = None
self.send_receive_stack = None
self.header_button_handler_id = None
self.pre_sign_widget = None
def on_activate(self, app):
ui_file_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"app.ui")
appwindow = 'applicationwindow1'
builder = Gtk.Builder()
builder.add_objects_from_file(ui_file_path, [appwindow])
window = builder.get_object(appwindow)
window.set_wmclass ("sign", "sign")
window.set_title("sign")
window.connect("delete-event", self.on_delete_window)
self.headerbar = window.get_titlebar()
self.header_button = builder.get_object("back_refresh_button")
self.header_button.connect('clicked', self.on_header_button_clicked)
sw = builder.get_object('stackswitcher1')
# I want to be able to press Alt+S and Alt+R respectively
# to switch the stack pages to Send and Receive.
# sw.get_children()
self.stack_switcher = sw
self.send_receive_stack = builder.get_object("send_receive_stack")
self.send_receive_stack.connect('notify::visible-child',
self.on_sr_stack_switch)
## Load Send part
self.send = SendApp()
ss = self.send.stack
p = ss.get_parent()
if p:
p.remove(ss)
ss.connect('notify::visible-child', self.on_send_stack_switch)
ss.connect('map', self.on_send_stack_mapped)
klw = self.send.klw
klw.connect("key-activated", self.on_key_activated)
klw.connect("map", self.on_keylist_mapped)
klw.props.margin_left = klw.props.margin_right = 15
self.send_stack = ss
## End of loading send part
# Load Receive part
self.receive = PswMappingReceiveApp(self.on_presign_mapped)
rs = self.receive.stack
rs.connect('notify::visible-child',
self.on_receive_stack_switch)
scanner = self.receive.scanner
scanner.connect("map", self.on_scanner_mapped)
self.receive_stack = rs
self.send_receive_stack.add_titled(self.send_stack,
"send_stack", _("Send"))
self.send_receive_stack.add_titled(rs,
"receive_stack", _("Receive"))
accel_group = Gtk.AccelGroup()
window.add_accel_group(accel_group)
self.receive.accept_button.add_accelerator("clicked", accel_group, ord('o'), Gdk.ModifierType.MOD1_MASK,
Gtk.AccelFlags.VISIBLE)
self.receive.accept_button.set_can_default(True)
window.show_all()
self.add_window(window)
Here's a hacky way to get to the first and last children of a stack with two children:
#!/usr/bin/env python
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GdkPixbuf, Gdk
import os, sys
class GUI:
def __init__(self):
self.stack = Gtk.Stack()
switcher = Gtk.StackSwitcher()
switcher.set_stack(self.stack)
label1 = Gtk.Label("label 1")
label2 = Gtk.Label("label 2")
label3 = Gtk.Label("label 3")
self.stack.add_titled (label1, "1", "Page 1")
self.stack.add_titled (label2, "2", "Page 2")
self.stack.add_titled (label3, "3", "Page 3")
box = Gtk.Box()
box.pack_start(switcher, True, False, 0)
box.pack_start(self.stack, True, True, 0)
box.set_orientation(Gtk.Orientation.VERTICAL)
window = Gtk.Window()
window.add(box)
window.show_all()
window.connect("key-press-event", self.key_press)
def key_press (self, window, event):
keyname = Gdk.keyval_name(event.keyval)
if not Gdk.ModifierType.CONTROL_MASK:
#only continue when the CTRL key is down
return
#get the last child
if keyname == "r":
for child in self.stack.get_children():
self.stack.set_visible_child(child)
#get the first child
if keyname == "s":
for child in self.stack.get_children():
self.stack.set_visible_child(child)
return
def on_window_destroy(self, window):
Gtk.main_quit()
def main():
app = GUI()
Gtk.main()
if __name__ == "__main__":
sys.exit(main())
Here's a hacky way to scroll forward and backward through a stack with more than two children:
#!/usr/bin/env python
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GdkPixbuf, Gdk
import os, sys
class GUI:
def __init__(self):
self.stack = Gtk.Stack()
switcher = Gtk.StackSwitcher()
switcher.set_stack(self.stack)
label1 = Gtk.Label("label 1")
label2 = Gtk.Label("label 2")
label3 = Gtk.Label("label 3")
self.stack.add_titled (label1, "1", "Page 1")
self.stack.add_titled (label2, "2", "Page 2")
self.stack.add_titled (label3, "3", "Page 3")
box = Gtk.Box()
box.pack_start(switcher, True, False, 0)
box.pack_start(self.stack, True, True, 0)
box.set_orientation(Gtk.Orientation.VERTICAL)
window = Gtk.Window()
window.add(box)
window.show_all()
window.connect("key-press-event", self.key_press)
def key_press (self, window, event):
keyname = Gdk.keyval_name(event.keyval)
if not Gdk.ModifierType.CONTROL_MASK:
#only continue when the CTRL key is down
return
#forward scroll
#variable to capture the active widget
previous_child_active = False
if keyname == "r":
#iterate over the stack children
for child in self.stack.get_children():
if previous_child_active == True:
# the last widget was the one that was active
self.stack.set_visible_child(child)
#finished
return
#remember if the previous child was active
previous_child_active = self.stack.get_visible_child() == child
#reverse scroll
#variable to capture the last widget we iterated
previous_child = None
if keyname == "s":
#iterate over the stack children
for child in self.stack.get_children():
if self.stack.get_visible_child() == child and previous_child != None:
#this is the active widget, so we set the previous active
self.stack.set_visible_child(previous_child)
#finished
return
#remember the last widget
previous_child = child
def on_window_destroy(self, window):
Gtk.main_quit()
def main():
app = GUI()
Gtk.main()
if __name__ == "__main__":
sys.exit(main())

I can't use the Gtk.get-text instruction correctly in python

I'm writing a small program in Python with Gtk3 and this is part of the code:
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class Layout(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="Ricerca partite")
self.set_default_size(265, 310)
self.connect("destroy", Gtk.main_quit)
layout = Gtk.Layout()
self.add(layout)
label = Gtk.Label("Inserisci i TAG della partita che cerchi")
layout.put(label, 10, 10)
xlabel = 10 ; ylabel = 50
xentry = 90 ; yentry = 45
yy=30
label = Gtk.Label(" Event ")
layout.put(label, xlabel, ylabel)
Event = Gtk.Entry()
Event.set_text("---test---")
layout.put(Event, xentry, yentry)
ylabel += yy ; yentry += yy
label = Gtk.Label(" Site ")
layout.put(label,xlabel, ylabel)
Site = Gtk.Entry()
layout.put(Site, xentry, yentry)
ylabel += yy ; yentry += yy
# ...
# Other 5 labels and entries
# ...
button = Gtk.Button(label=" cerca ")
layout.put(button, 186, ylabel+10)
button.connect("clicked", self.on_button_clicked)
txt=Event.get_text() # I verify "Event" entry
print(txt)
def on_button_clicked(self, button):
print("Button has been clicked!")
txt=window.Event.get_text()
print(ddd,type(ddd))
window = Layout()
window.show_all()
Gtk.main()
When the "Search" button is clicked I print "Button has been clicked"! and I would like to put in as many variables the content of the seven "entry" but, forgive my ignorance, despite all the trials, I can't get the result I want.
The error I receive is:
Traceback (most recent call last):
File "scriptName.py", online 75, in on_button_clicked
txt=window.Event.get_text ()
AttributeError:' Layout' object has no attribute' Event'.
Thank you and apologize for the bad English: translated with PC.
Translated with www.DeepL.com/Translator
You may want to read up on Python classes sometime. Anyway, you were accessing a new class from inside the first class, leading to your error. Here is the proper code:
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class Layout(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="Ricerca partite")
self.set_default_size(265, 310)
self.connect("destroy", Gtk.main_quit)
layout = Gtk.Layout()
self.add(layout)
label = Gtk.Label("Inserisci i TAG della partita che cerchi")
layout.put(label, 10, 10)
xlabel = 10 ; ylabel = 50
xentry = 90 ; yentry = 45
yy=30
label = Gtk.Label(" Event ")
layout.put(label, xlabel, ylabel)
self.Event = Gtk.Entry()
self.Event.set_text("---test---")
layout.put(self.Event, xentry, yentry)
ylabel += yy ; yentry += yy
label = Gtk.Label(" Site ")
layout.put(label,xlabel, ylabel)
Site = Gtk.Entry()
layout.put(Site, xentry, yentry)
ylabel += yy ; yentry += yy
# ...
# Other 5 labels and entries
# ...
button = Gtk.Button(label=" cerca ")
layout.put(button, 186, ylabel+10)
button.connect("clicked", self.on_button_clicked)
txt=self.Event.get_text() # I verify "Event" entry
print(txt)
def on_button_clicked(self, button):
print("Button has been clicked!")
text=self.Event.get_text()
print(text,type(text))
window = Layout()
window.show_all()
Gtk.main()

Triggering and updating a docked window

I am trying to control some valves with a GUI and update the display when the button is pressed. The following code produces the correct initial display, but when I click one of the checkboxes I always get the value of i=2 and when unchecked I get i=0. I also have no idea how to update the docked window so that the message will toggle from close to open and vise versa. I do see the resultant value in self.states change in the array, but am always left with ['open', 'closed', 'open', 'closed', 'closed', 'closed'] Though this is changed the red closed on the right of the screen does not change to `open
class ApplicationWindow(gui.QMainWindow):
def __init__(self):
self.nCheckBoxes=6
self.states = ['closed']*self.nCheckBoxes
gui.QMainWindow.__init__(self)
self.setAttribute(core.Qt.WA_DeleteOnClose)
self.setWindowTitle("PiView")
self.file_menu = gui.QMenu('&File', self)
self.file_menu.addAction('&Quit', self.fileQuit, core.Qt.CTRL + core.Qt.Key_Q)
self.menuBar().addMenu(self.file_menu)
self.help_menu = gui.QMenu('&Help', self)
self.menuBar().addSeparator()
self.menuBar().addMenu(self.help_menu)
self.help_menu.addAction('&About', self.about)
self.help_menu.addAction('&Docs', self.doc)
self.main_widget = gui.QWidget(self)
l = gui.QVBoxLayout(self.main_widget)
self.dc = MyMplCanvas(self.main_widget, width=5, height=4, dpi=100)
l.addWidget(self.dc)
self.main_widget.setFocus()
self.setCentralWidget(self.main_widget)
self.dw = self.createDockWindows(self.dc)
self.statusBar().showMessage("Initialized", 2000)
def createDockWindows(self, MyMplCanvas):
"""
Create all the dock widgets
"""
self.drawValves()
self.drawGraphAdjustments()
self.drawStartAndStop()
def drawValves(self):
cboxes = gui.QDockWidget("Controls", self)
cboxes.setAllowedAreas(core.Qt.LeftDockWidgetArea)
w = gui.QWidget()
#layout = gui.QVBoxLayout()
layout = gui.QGridLayout()
w.setLayout(layout)
self.c = [0]*self.nCheckBoxes
# Create self.nCheckBoxes
msgBox = gui.QLabel()
msgBox.setText("States")
font = gui.QFont()
font.setBold(True)
msgBox.setFont(font)
msgBox.setStyleSheet("color: rgb(255,0,0)")
layout.addWidget(msgBox,0,1)
for i in range(self.nCheckBoxes):
self.c[i] = gui.QCheckBox("Valve " + str(i))
self.c[i].setChecked(False)
#self.c[i].stateChanged.connect(lambda:self.btnstate(self.c[i]))
#self.c[i].stateChanged.connect(self.checkedBox)
self.c[i].stateChanged.connect(lambda i: self.checkedBox(i))
layout.addWidget(self.c[i],i+1,0)
# Messages
msgBox = gui.QLabel()
msgBox.setText(self.states[i])
if self.states[i] == 'closed':
msgBox.setStyleSheet("color: rgb(255,0,0)")
else:
msgBox.setStyleSheet("color: rgb(0,255,0)")
layout.addWidget(msgBox,i+1,1)
spacerItem = gui.QSpacerItem(20,40, gui.QSizePolicy.Minimum, gui.QSizePolicy.Expanding)
layout.addItem(spacerItem)
cboxes.setWidget(w)
self.addDockWidget(core.Qt.LeftDockWidgetArea, cboxes)
def checkedBox(self,i):
sender = self.sender()
self.states[i] = 'open' if 'close' else 'close'
# For debugging
print i
print self.states
If I understand correctly I can just actuate the valve like I would normally in the function checkedBox.
This is what is displayed
I want to be able to click a checkbox and the message just to the right to change from closed to open, and vise versa.
The problem arises because the default value of i takes the value sent by the signal (ie state: Unchecked = 0, PartiallyChecked = 1, Checked = 2), to solve this we will add a further value to the function: Index of the checkbox. Also if you want to change the text of QLabel you must be able to access them so I have created a new list with labels. To make the change we will use the states list, for this implement the function updateLabels.
class ApplicationWindow(gui.QMainWindow):
def __init__(self):
self.nCheckBoxes=6
self.states = ['closed']*self.nCheckBoxes
gui.QMainWindow.__init__(self)
self.setAttribute(core.Qt.WA_DeleteOnClose)
self.setWindowTitle("PiView")
self.file_menu = gui.QMenu('&File', self)
#self.file_menu.addAction('&Quit', self.fileQuit, core.Qt.CTRL + core.Qt.Key_Q)
self.menuBar().addMenu(self.file_menu)
self.help_menu = gui.QMenu('&Help', self)
self.menuBar().addSeparator()
self.menuBar().addMenu(self.help_menu)
#self.help_menu.addAction('&About', self.about)
#self.help_menu.addAction('&Docs', self.doc)
self.main_widget = gui.QWidget(self)
l = gui.QVBoxLayout(self.main_widget)
self.dc = MyMplCanvas(self.main_widget, width=5, height=4, dpi=100)
l.addWidget(self.dc)
self.main_widget.setFocus()
self.setCentralWidget(self.main_widget)
self.dw = self.createDockWindows(self.dc)
self.statusBar().showMessage("Initialized", 2000)
def createDockWindows(self, MyMplCanvas):
"""
Create all the dock widgets
"""
self.drawValves()
#self.drawGraphAdjustments()
#self.drawStartAndStop()
def drawValves(self):
cboxes = gui.QDockWidget("Controls", self)
cboxes.setAllowedAreas(core.Qt.LeftDockWidgetArea)
w = gui.QWidget()
#layout = gui.QVBoxLayout()
layout = gui.QGridLayout()
w.setLayout(layout)
self.c = []
self.l = []
# Create self.nCheckBoxes
msgBox = gui.QLabel()
msgBox.setText("States")
font = gui.QFont()
font.setBold(True)
msgBox.setFont(font)
msgBox.setStyleSheet("color: rgb(255,0,0)")
layout.addWidget(msgBox,0,1)
for i in range(self.nCheckBoxes):
checkBox = gui.QCheckBox("Valve " + str(i))
checkBox.setChecked(False)
checkBox.stateChanged.connect(lambda state, p=i: self.checkedBox(state, p))
self.c.append(checkBox)
layout.addWidget(self.c[i],i+1,0)
msgBox = gui.QLabel()
msgBox.setText(self.states[i])
self.l.append(msgBox)
layout.addWidget(msgBox,i+1,1)
self.update_labels()
spacerItem = gui.QSpacerItem(20,40, gui.QSizePolicy.Minimum, gui.QSizePolicy.Expanding)
layout.addItem(spacerItem)
cboxes.setWidget(w)
self.addDockWidget(core.Qt.LeftDockWidgetArea, cboxes)
def checkedBox(self, state, p):
if state == core.Qt.Unchecked:
self.states[p] = 'closed'
else:
self.states[p] = 'open'
self.update_labels()
def update_labels(self):
for i in range(self.nCheckBoxes):
if self.states[i] == 'closed':
text = "closed"
styleSheet = "color: rgb(255,0,0)"
else:
text = "open"
styleSheet = "color: rgb(0,255,0)"
self.l[i].setText(text)
self.l[i].setStyleSheet(styleSheet)

How can I record the position where the user pressed the mouse button with Python and GTK?

I would like to let the user of my application draw something. Which signal / event do I use for that?
This is what I've got so far:
#!/usr/bin/env python
import gtk
class DrawingAreaExample:
def __init__(self):
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
window.set_title("Drawing Area Example")
window.set_default_size(800, 600)
window.connect('button-press-event', self.callback)
window.connect("destroy", lambda w: gtk.main_quit())
self.area = gtk.DrawingArea()
self.area.set_size_request(400, 300)
self.pangolayout = self.area.create_pango_layout("")
self.sw = gtk.ScrolledWindow()
self.sw.add_with_viewport(self.area)
self.table = gtk.Table(2, 2)
self.table.attach(self.sw, 1, 2, 1, 2)
window.add(self.table)
self.area.connect("expose-event", self.area_expose_cb)
self.area.show()
self.sw.show()
self.table.show()
window.show()
def callback(self, window, event):
self.draw_point(int(event.x), int(event.y))
def area_expose_cb(self, area, event):
self.style = self.area.get_style()
self.gc = self.style.fg_gc[gtk.STATE_NORMAL]
return True
def draw_point(self, x, y):
# self.area.window.draw_point(self.gc, x, y)
self.area.window.draw_arc(self.gc, True, x, y, 5, 5, 5, 360*64)
def main():
gtk.main()
return 0
if __name__ == "__main__":
DrawingAreaExample()
main()
I just got it to work. Thanks to http://zetcode.com/gfx/pycairo/basicdrawing/ for the tutorial which helped me a lot!
#!/usr/bin/python
"""Write on a canvas, store on-line data."""
from gi.repository import Gtk, Gdk
import time
import math
__version__ = '0.1'
class FormulaWriter(Gtk.Window):
def __init__(self):
super(FormulaWriter, self).__init__()
self.odata = [] # On-line writing information, grouped by strokes
# General properties
self.set_title("Formula Writer %s" % __version__)
self.resize(400, 400)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", Gtk.main_quit)
# Set up canvas
self.canvas = Gtk.DrawingArea()
self.canvas.connect("draw", self.on_draw)
self.canvas.connect("button-press-event", self.on_button_press)
self.canvas.connect("motion-notify-event", self.on_mouse_move)
self.canvas.connect("motion-notify-event", self.on_mouse_move)
self.canvas.set_events(self.canvas.get_events() |
Gdk.EventMask.BUTTON_MOTION_MASK |
Gdk.EventMask.BUTTON1_MOTION_MASK |
Gdk.EventMask.BUTTON2_MOTION_MASK |
Gdk.EventMask.BUTTON3_MOTION_MASK |
Gdk.EventMask.BUTTON_PRESS_MASK)
self.add(self.canvas)
self.show_all()
def on_button_press(self, w, event):
"""When a button is pressed, the location gets stored and the canvas
gets updated.
"""
self.odata.append([{'x': event.x, 'y': event.y, 'time': time.time()}])
self.canvas.queue_draw()
def on_mouse_move(self, w, event):
"""When mouse is moved, the mouse position gets stored."""
point = {'x': event.x, 'y': event.y, 'time': time.time()}
self.odata[-1].append(point)
self.canvas.queue_draw()
def on_draw(self, wid, cr):
"""Handler for drawing action. Draw all strokes.
:param wid: The DrawingArea
:param cr: Context
"""
cr.set_source_rgb(1, 0, 0) # All strokes get drawn in red
cr.set_line_width(2.5)
for stroke in self.odata:
for i, point in enumerate(stroke):
if len(stroke) == 1:
radius = 2
cr.arc(point['x'], point['y'], radius, 0, 2.0*math.pi)
cr.fill()
cr.stroke()
elif i != 0:
cr.move_to(stroke[i-1]['x'], stroke[i-1]['y'])
cr.line_to(point['x'], point['y'])
cr.stroke()
def main():
FormulaWriter()
Gtk.main()
if __name__ == "__main__":
main()
button-press-event is correct, but you want to hook it up to the GtkDrawingArea, not to the GtkWindow. You'll also need motion-notify-event to handle mouse moves during the button press.
The tutorial here will show you how to do what you want to do. There are a few other slight problems with your code (such as the use of expose-event instead of draw) that this code should show how to do right. Unfortunately it's in C; IDK if the Python GTK+ 3 tutorial has the same example.

Display message when hovering over something with mouse cursor in Python

I have a GUI made with TKinter in Python. I would like to be able to display a message when my mouse cursor goes, for example, on top of a label or button. The purpose of this is to explain to the user what the button/label does or represents.
Is there a way to display text when hovering over a tkinter object in Python?
I think this would meet your requirements.
Here's what the output looks like:
First, A class named ToolTip which has methods showtip and hidetip is defined as follows:
from tkinter import *
class ToolTip(object):
def __init__(self, widget):
self.widget = widget
self.tipwindow = None
self.id = None
self.x = self.y = 0
def showtip(self, text):
"Display text in tooltip window"
self.text = text
if self.tipwindow or not self.text:
return
x, y, cx, cy = self.widget.bbox("insert")
x = x + self.widget.winfo_rootx() + 57
y = y + cy + self.widget.winfo_rooty() +27
self.tipwindow = tw = Toplevel(self.widget)
tw.wm_overrideredirect(1)
tw.wm_geometry("+%d+%d" % (x, y))
label = Label(tw, text=self.text, justify=LEFT,
background="#ffffe0", relief=SOLID, borderwidth=1,
font=("tahoma", "8", "normal"))
label.pack(ipadx=1)
def hidetip(self):
tw = self.tipwindow
self.tipwindow = None
if tw:
tw.destroy()
def CreateToolTip(widget, text):
toolTip = ToolTip(widget)
def enter(event):
toolTip.showtip(text)
def leave(event):
toolTip.hidetip()
widget.bind('<Enter>', enter)
widget.bind('<Leave>', leave)
The widget is where you want to add the tip. For example, if you want the tip when you hover over a button or entry or label, the instance of the same should be provided at the call time.
Quick note: the code above uses from tkinter import *
which is not suggested by some of the programmers out there, and they have valid points. You might want to make necessary changes in such case.
To move the tip to your desired location, you can change x and y in the code.
The function CreateToolTip() helps to create this tip easily. Just pass the widget and string you want to display in the tipbox to this function, and you're good to go.
This is how you call the above part:
button = Button(root, text = 'click mem')
button.pack()
CreateToolTip(button, text = 'Hello World\n'
'This is how tip looks like.'
'Best part is, it\'s not a menu.\n'
'Purely tipbox.')
Do not forget to import the module if you save the previous outline in different python file, and don't save the file as CreateToolTip or ToolTip to avoid confusion.
This post from Fuzzyman shares some similar thoughts, and worth checking out.
You need to set a binding on the <Enter> and <Leave> events.
Note: if you choose to pop up a window (ie: a tooltip) make sure you don't pop it up directly under the mouse. What will happen is that it will cause a leave event to fire because the cursor leaves the label and enters the popup. Then, your leave handler will dismiss the window, your cursor will enter the label, which causes an enter event, which pops up the window, which causes a leave event, which dismisses the window, which causes an enter event, ... ad infinitum.
For simplicity, here's an example that updates a label, similar to a statusbar that some apps use. Creating a tooltip or some other way of displaying the information still starts with the same core technique of binding to <Enter> and <Leave>.
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.l1 = tk.Label(self, text="Hover over me")
self.l2 = tk.Label(self, text="", width=40)
self.l1.pack(side="top")
self.l2.pack(side="top", fill="x")
self.l1.bind("<Enter>", self.on_enter)
self.l1.bind("<Leave>", self.on_leave)
def on_enter(self, event):
self.l2.configure(text="Hello world")
def on_leave(self, enter):
self.l2.configure(text="")
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand="true")
root.mainloop()
You can refer to this- HoverClass
It is exactly what you need. Nothing more, nothing less
from Tkinter import *
import re
class HoverInfo(Menu):
def __init__(self, parent, text, command=None):
self._com = command
Menu.__init__(self,parent, tearoff=0)
if not isinstance(text, str):
raise TypeError('Trying to initialise a Hover Menu with a non string type: ' + text.__class__.__name__)
toktext=re.split('\n', text)
for t in toktext:
self.add_command(label = t)
self._displayed=False
self.master.bind("<Enter>",self.Display )
self.master.bind("<Leave>",self.Remove )
def __del__(self):
self.master.unbind("<Enter>")
self.master.unbind("<Leave>")
def Display(self,event):
if not self._displayed:
self._displayed=True
self.post(event.x_root, event.y_root)
if self._com != None:
self.master.unbind_all("<Return>")
self.master.bind_all("<Return>", self.Click)
def Remove(self, event):
if self._displayed:
self._displayed=False
self.unpost()
if self._com != None:
self.unbind_all("<Return>")
def Click(self, event):
self._com()
Example app using HoverInfo:
from Tkinter import *
from HoverInfo import HoverInfo
class MyApp(Frame):
def __init__(self, parent=None):
Frame.__init__(self, parent)
self.grid()
self.lbl = Label(self, text='testing')
self.lbl.grid()
self.hover = HoverInfo(self, 'while hovering press return \n for an exciting msg', self.HelloWorld)
def HelloWorld(self):
print('Hello World')
app = MyApp()
app.master.title('test')
app.mainloop()
Screenshot:
I have a very hacky solution but it has some advantages over the current answers so I figured I would share it.
lab=Label(root,text="hover me")
lab.bind("<Enter>",popup)
def do_popup(event):
# display the popup menu
root.after(1000, self.check)
popup = Menu(root, tearoff=0)
popup.add_command(label="Next")
popup.tk_popup(event.x_root, event.y_root, 0)
def check(event=None):
x, y = root.winfo_pointerxy()
widget = root.winfo_containing(x, y)
if widget is None:
root.after(100, root.check)
else:
leave()
def leave():
popup.delete(0, END)
The only real issue with this is it leaves behind a small box that moves focus away from the main window
If anyone knows how to solve these issues let me know
If anyone is on Mac OSX and tool tip isn't working, check out the example in:
https://github.com/python/cpython/blob/master/Lib/idlelib/tooltip.py
Basically, the two lines that made it work for me on Mac OSX were:
tw.update_idletasks() # Needed on MacOS -- see #34275.
tw.lift() # work around bug in Tk 8.5.18+ (issue #24570)
Here is a simple solution to your problem that subclasses the tk.Button object. We make a special class that tk.Button inherits from, giving it tooltip functionality. The same for tk.Labels.
I don't know what would be cleanest and the easiest way to maintain code for keeping track of the text that goes into the tooltips. I present here one way, in which I pass unique widget IDs to MyButtons, and access a dictionary for storing the tooltip texts. You could store this file as a JSON, or as a class attribute, or as a global variable (as below). Alternatively, perhaps it would be better to define a setter method in MyButton, and just call this method every time you create a new widget that should have a tooltip. Although you would have to store the widget instance in a variable, adding one extra line for all widgets to include.
One drawback in the code below is that the self.master.master syntax relies on determining the "widget depth". A simple recursive function will catch most cases (only needed for entering a widget, since by definition you leave somewhere you once were).
Anyway, below is a working MWE for anyone interested.
import tkinter as tk
tooltips = {
'button_hello': 'Print a greeting message',
'button_quit': 'Quit the program',
'button_insult': 'Print an insult',
'idle': 'Hover over button for help',
'error': 'Widget ID not valid'
}
class ToolTipFunctionality:
def __init__(self, wid):
self.wid = wid
self.widet_depth = 1
self.widget_search_depth = 10
self.bind('<Enter>', lambda event, i=1: self.on_enter(event, i))
self.bind('<Leave>', lambda event: self.on_leave(event))
def on_enter(self, event, i):
if i > self.widget_search_depth:
return
try:
cmd = f'self{".master"*i}.show_tooltip(self.wid)'
eval(cmd)
self.widget_depth = i
except AttributeError:
return self.on_enter(event, i+1)
def on_leave(self, event):
cmd = f'self{".master" * self.widget_depth}.hide_tooltip()'
eval(cmd)
class MyButton(tk.Button, ToolTipFunctionality):
def __init__(self, parent, wid, **kwargs):
tk.Button.__init__(self, parent, **kwargs)
ToolTipFunctionality.__init__(self, wid)
class MyLabel(tk.Label, ToolTipFunctionality):
def __init__(self, parent, wid, **kwargs):
tk.Label.__init__(self, parent, **kwargs)
ToolTipFunctionality.__init__(self, wid)
class Application(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.tooltip = tk.StringVar()
self.tooltip.set(tooltips['idle'])
self.frame = tk.Frame(self, width=50)
self.frame.pack(expand=True)
MyLabel(self.frame, '', text='One Cool Program').pack()
self.subframe = tk.Frame(self.frame, width=40)
self.subframe.pack()
MyButton(self.subframe, 'button_hello', text='Hello!', command=self.greet, width=20).pack()
MyButton(self.subframe, 'button_insutl', text='Insult', command=self.insult, width=20).pack()
MyButton(self.subframe, 'button_quit', text='Quit', command=self.destroy, width=20).pack()
tk.Label(self.subframe, textvar=self.tooltip, width=20).pack()
def show_tooltip(self, wid):
try:
self.tooltip.set(tooltips[wid])
except KeyError:
self.tooltip.set(tooltips['error'])
def hide_tooltip(self):
self.tooltip.set(tooltips['idle'])
def greet(self):
print('Welcome, Fine Sir!')
def insult(self):
print('You must be dead from the neck up')
if __name__ == '__main__':
app = Application()
app.mainloop()
The best way I have found to create a popup help window is to use the tix.Balloon. I have modified it below to make it look better and show an example (note the use of tix.Tk):
import tkinter as tk
import tkinter.tix as tix
class Balloon(tix.Balloon):
# A modified tix popup balloon (to change the default delay, bg and wraplength)
init_after = 1250 # Milliseconds
wraplength = 300 # Pixels
def __init__(self, master):
bg = root.cget("bg")
# Call the parent
super().__init__(master, initwait=self.init_after)
# Change background colour
for i in self.subwidgets_all():
i.config(bg=bg)
# Modify the balloon label
self.message.config(wraplength=self.wraplength)
root = tix.Tk()
l = tk.Label(root, text="\n".join(["text"] * 5))
l.pack()
b = Balloon(root.winfo_toplevel())
b.bind_widget(l, balloonmsg="Some random text")
root.mainloop()
OLD ANSWER:
Here is an example using <enter> and <leave> as #bryanoakley suggested with a toplevel (with overridedirect set to true). Use the hover_timer class for easy use of this. This needs the widget and help-text (with an optional delay argument - default 0.5s) and can be easily called just by initiating the class and then cancelling it.
import threading, time
from tkinter import *
class hover_window (Toplevel):
def __init__ (self, coords, text):
super ().__init__ ()
self.geometry ("+%d+%d" % (coords [0], coords [1]))
self.config (bg = "white")
Label (self, text = text, bg = "white", relief = "ridge", borderwidth = 3, wraplength = 400, justify = "left").grid ()
self.overrideredirect (True)
self.update ()
self.bind ("<Enter>", lambda event: self.destroy ())
class hover_timer:
def __init__ (self, widget, text, delay = 2):
self.wind, self.cancel_var, self.widget, self.text, self.active, self.delay = None, False, widget, text, False, delay
threading.Thread (target = self.start_timer).start ()
def start_timer (self):
self.active = True
time.sleep (self.delay)
if not self.cancel_var: self.wind = hover_window ((self.widget.winfo_rootx (), self.widget.winfo_rooty () + self.widget.winfo_height () + 20), self.text)
self.active = False
def delayed_stop (self):
while self.active: time.sleep (0.05)
if self.wind:
self.wind.destroy ()
self.wind = None
def cancel (self):
self.cancel_var = True
if not self.wind: threading.Thread (target = self.delayed_stop).start ()
else:
self.wind.destroy ()
self.wind = None
def start_help (event):
# Create a new help timer
global h
h = hover_timer (l, "This is some additional information.", 0.5)
def end_help (event):
# If therre is one, end the help timer
if h: h.cancel ()
if __name__ == "__main__":
# Create the tkinter window
root = Tk ()
root.title ("Hover example")
# Help class not created yet
h = None
# Padding round label
Frame (root, width = 50).grid (row = 1, column = 0)
Frame (root, height = 50).grid (row = 0, column = 1)
Frame (root, width = 50).grid (row = 1, column = 2)
Frame (root, height = 50).grid (row = 2, column = 1)
# Setup the label
l = Label (root, text = "Hover over me for information.", font = ("sans", 32))
l.grid (row = 1, column = 1)
l.bind ("<Enter>", start_help)
l.bind ("<Leave>", end_help)
# Tkinter mainloop
root.mainloop ()
I wanted to contribute to the answer of #squareRoot17 as he inspired me to shorten his code while providing the same functionality:
import tkinter as tk
class ToolTip(object):
def __init__(self, widget, text):
self.widget = widget
self.text = text
def enter(event):
self.showTooltip()
def leave(event):
self.hideTooltip()
widget.bind('<Enter>', enter)
widget.bind('<Leave>', leave)
def showTooltip(self):
self.tooltipwindow = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(1) # window without border and no normal means of closing
tw.wm_geometry("+{}+{}".format(self.widget.winfo_rootx(), self.widget.winfo_rooty()))
label = tk.Label(tw, text = self.text, background = "#ffffe0", relief = 'solid', borderwidth = 1).pack()
def hideTooltip(self):
tw = self.tooltipwindow
tw.destroy()
self.tooltipwindow = None
This class can then be imported and used as:
import tkinter as tk
from tooltip import ToolTip
root = tk.Tk()
your_widget = tk.Button(root, text = "Hover me!")
ToolTip(widget = your_widget, text = "Hover text!")
root.mainloop()

Categories

Resources