I have multiple Tkinter.labels in a row and i would like the user to be able to click and drag their mouse over each one, activating them in the process.
I know about bindings, but i need multiple events in a single binding. Ive been messing around with <Button-1> and <Enter>, however i need a callback to be called only when both are true.
I know l.bind('<Button-1>,<Enter>', ...) is not valid.
Anyone with more Tkinter experience know of a way to chain binding, or make a multi-bind??
The way you solve this particular problem is to have a binding on ButtonPress and ButtonRelease that sets a flag. Then, in your binding for <Enter> (or any other event) you check for that flag.
However, while the button is pressed you won't get any <Enter> events. This is because the widget you clicked over grabs the pointer and owns it until you release the button. The only <Enter> events you'll get while the button is pressed are when you enter the widget you originally clicked on.
What you want to do instead is bind to <B1-Motion>. You can then use the x/y coordinates of the event and winfo_containing to determine what widget you are over.
That being said, trying to simulate selection over a row of labels is a lot of work for very little benefit. Why not just use a text widget that already has selection built in? You can tweak it so that it looks like a bunch of labels (ie: make the background the same color as a frame) and you can turn editing off. That might be an easier way to go.
I encountered this same problem today and thanks to #Bryan Oakley's answer I was able to code a working solution. I will share my code in the hope that it will help someone someday.
This example builds 2 tkinter TreeViews, and enables dragging-and-dropping treeItems between the 2 trees. The key point is that by binding both trees to the B1-motion event, both trees are able to respond to the events.
import tkinter as tk
from tkinter import ttk
from tkinter.messagebox import showinfo
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class TreeItem:
"""
Keeps a reference to a treeItem together with its parent tree.
"""
def __init__(self, tree, item):
self.tree = tree
self.item = item
self.itemTxt = tree.item(item,"text")
def __str__(self):
"""
Prints 'treename, itemname' upon calling str(TreeItem)
"""
return f'{self.tree}, {self.itemTxt}'
class Mouse(metaclass=Singleton):
"""
Handles treeitem clicking, dragging, dropping and shows feedback messages about them.
"""
def __init__(self, root):
self.root = root
self.clicked_item = None
self.is_dragging = False
self.drag_time = 0
self.current_hovering_widget = None
def OnMouseDown(self, event):
clicked_item = self.get_item_under_mouse(event)
print("You clicked on", str(clicked_item))
self.clicked_item = clicked_item
def OnDrag(self, event):
self.is_dragging = True
self.show_drag_init_msg()
self.show_hovering_item_change_msg(event)
self.drag_time += 1
def OnMouseUp(self, event):
if self.is_dragging:
self.finish_drag()
self.show_drop_msg()
self.clicked_item = None
def finish_drag(self):
self.is_dragging = False
self.drag_time = 0
def show_drag_init_msg(self):
if self.drag_time == 0:
print("You are now dragging item", self.clicked_item.tree, self.clicked_item.itemTxt)
def show_hovering_item_change_msg(self, event):
currently_hovering = self.get_item_under_mouse(event)
if str(self.current_hovering_widget) != str(currently_hovering):
print("Mouse is above", str(currently_hovering))
self.current_hovering_widget = currently_hovering
def show_drop_msg(self):
dragged_item:TreeItem = self.clicked_item
dragged_onto:TreeItem = self.current_hovering_widget
print(f'You dropped {str(dragged_item)} onto {str(dragged_onto)}')
def get_item_under_mouse(self, event):
current_tree = self.root.winfo_containing(event.x_root, event.y_root)
current_tree_item = current_tree.identify("item", event.x, event.y)
return TreeItem(tree=current_tree, item=current_tree_item)
class Tree:
def __init__(self, root, row, col):
self.root: tk.Tk = root
self.create_tree(root, row, col)
def OnDrag(self,event):
Mouse(self.root).OnDrag(event)
def OnMouseDown(self, event):
Mouse(self.root).OnMouseDown(event)
def OnMouseUp(self, event):
Mouse(self.root).OnMouseUp(event)
def create_tree(self, root, row, col):
self.tree = ttk.Treeview(root)
self.tree.heading('#0', text='Departments', anchor='w')
self.tree.grid(row=row, column=col, sticky='nsew')
self.add_dummy_data()
# add bindings
self.tree.bind("<ButtonPress-1>", self.OnMouseDown)
self.tree.bind("<ButtonRelease-1>", self.OnMouseUp)
self.tree.bind("<B1-Motion>", self.OnDrag)
def add_dummy_data(self):
# adding data
self.tree.insert('', tk.END, text='Administration', iid=0, open=False)
self.tree.insert('', tk.END, text='Logistics', iid=1, open=False)
self.tree.insert('', tk.END, text='Sales', iid=2, open=False)
self.tree.insert('', tk.END, text='Finance', iid=3, open=False)
self.tree.insert('', tk.END, text='IT', iid=4, open=False)
# adding children of first node
self.tree.insert('', tk.END, text='John Doe', iid=5, open=False)
self.tree.insert('', tk.END, text='Jane Doe', iid=6, open=False)
self.tree.move(5, 0, 0)
self.tree.move(6, 0, 1)
root = tk.Tk()
root.geometry('620x200')
# make two trees
tree1 = Tree(root,0,0)
tree2 = Tree(root,0,1)
# run the app
root.mainloop()
Related
I searched for ways of implementing tooltips for an application and I found in some comment or answer in this site some while ago a link to this page.
I've been using this class since then and I've been happy with the result.
But recently I noticed that the tooltips came up behind modal windows, when they refer to widgets on that modal window.
Below in the code downloaded from that GitHub link, where I just made the changes of replacing from tkinter import * with import tkinter as tk, and using the prefix tk throughout the code accordingly.
"""Tools for displaying tool-tips.
This includes:
* an abstract base-class for different kinds of tooltips
* a simple text-only Tooltip class
"""
import tkinter as tk
class TooltipBase:
"""abstract base class for tooltips"""
def __init__(self, anchor_widget):
"""Create a tooltip.
anchor_widget: the widget next to which the tooltip will be shown
Note that a widget will only be shown when showtip() is called.
"""
self.anchor_widget = anchor_widget
self.tipwindow = None
def __del__(self):
self.hidetip()
def showtip(self):
"""display the tooltip"""
if self.tipwindow:
return
self.tipwindow = tw = tk.Toplevel(self.anchor_widget)
# show no border on the top level window
tw.wm_overrideredirect(1)
try:
# This command is only needed and available on Tk >= 8.4.0 for OSX.
# Without it, call tips intrude on the typing process by grabbing
# the focus.
tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w,
"help", "noActivates")
except tk.TclError:
pass
self.position_window()
self.showcontents()
self.tipwindow.update_idletasks() # Needed on MacOS -- see #34275.
self.tipwindow.lift() # work around bug in Tk 8.5.18+ (issue #24570)
def position_window(self):
"""(re)-set the tooltip's screen position"""
x, y = self.get_position()
root_x = self.anchor_widget.winfo_rootx() + x
root_y = self.anchor_widget.winfo_rooty() + y
self.tipwindow.wm_geometry("+%d+%d" % (root_x, root_y))
def get_position(self):
"""choose a screen position for the tooltip"""
# The tip window must be completely outside the anchor widget;
# otherwise when the mouse enters the tip window we get
# a leave event and it disappears, and then we get an enter
# event and it reappears, and so on forever :-(
#
# Note: This is a simplistic implementation; sub-classes will likely
# want to override this.
return 20, self.anchor_widget.winfo_height() + 1
def showcontents(self):
"""content display hook for sub-classes"""
# See ToolTip for an example
raise NotImplementedError
def hidetip(self):
"""hide the tooltip"""
# Note: This is called by __del__, so careful when overriding/extending
tw = self.tipwindow
self.tipwindow = None
if tw:
try:
tw.destroy()
except tk.TclError: # pragma: no cover
pass
class OnHoverTooltipBase(TooltipBase):
"""abstract base class for tooltips, with delayed on-hover display"""
def __init__(self, anchor_widget, hover_delay=1000):
"""Create a tooltip with a mouse hover delay.
anchor_widget: the widget next to which the tooltip will be shown
hover_delay: time to delay before showing the tooltip, in milliseconds
Note that a widget will only be shown when showtip() is called,
e.g. after hovering over the anchor widget with the mouse for enough
time.
"""
super(OnHoverTooltipBase, self).__init__(anchor_widget)
self.hover_delay = hover_delay
self._after_id = None
self._id1 = self.anchor_widget.bind("<Enter>", self._show_event)
self._id2 = self.anchor_widget.bind("<Leave>", self._hide_event)
self._id3 = self.anchor_widget.bind("<Button>", self._hide_event)
def __del__(self):
try:
self.anchor_widget.unbind("<Enter>", self._id1)
self.anchor_widget.unbind("<Leave>", self._id2) # pragma: no cover
self.anchor_widget.unbind("<Button>", self._id3) # pragma: no cover
except tk.TclError: # pragma: no cover
pass
super(OnHoverTooltipBase, self).__del__()
def _show_event(self, event=None):
"""event handler to display the tooltip"""
if self.hover_delay:
self.schedule()
else:
self.showtip()
def _hide_event(self, event=None):
"""event handler to hide the tooltip"""
self.hidetip()
def schedule(self):
"""schedule the future display of the tooltip"""
self.unschedule()
self._after_id = self.anchor_widget.after(self.hover_delay,
self.showtip)
def unschedule(self):
"""cancel the future display of the tooltip"""
after_id = self._after_id
self._after_id = None
if after_id:
self.anchor_widget.after_cancel(after_id)
def hidetip(self):
"""hide the tooltip"""
try:
self.unschedule()
except tk.TclError: # pragma: no cover
pass
super(OnHoverTooltipBase, self).hidetip()
def showcontents(self):
"""content display hook for sub-classes"""
# See ToolTip for an example
raise NotImplementedError
class Hovertip(OnHoverTooltipBase):
"""A tooltip that pops up when a mouse hovers over an anchor widget."""
def __init__(self, anchor_widget, text, hover_delay=1000):
"""Create a text tooltip with a mouse hover delay.
anchor_widget: the widget next to which the tooltip will be shown
hover_delay: time to delay before showing the tooltip, in milliseconds
Note that a widget will only be shown when showtip() is called,
e.g. after hovering over the anchor widget with the mouse for enough
time.
"""
super(Hovertip, self).__init__(anchor_widget, hover_delay=hover_delay)
self.text = text
def showcontents(self):
label = tk.Label(self.tipwindow, text=self.text, justify=tk.LEFT,
background="#ffffe0", relief=tk.SOLID, borderwidth=1)
label.pack()
Now some code illustrating the problem I'm having:
class PopupWindow:
def __init__(self, parent):
self.parent = parent
self.gui = tk.Toplevel(self.parent)
self.gui.geometry("100x30")
self.gui.wait_visibility()
self.ok_button = tk.Button(self.gui, text="OK", command=self.on_ok_button)
self.ok_button.pack()
Hovertip(self.ok_button, text="OK button", hover_delay=500)
def on_ok_button(self):
self.gui.destroy()
def show(self):
self.gui.grab_set()
# Hovertip(self.ok_button, text="OK button", hover_delay=500)
self.gui.wait_window()
return 0
class App(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
button = tk.Button(parent, text="Button -- no hover delay", command=self.button)
button.pack()
Hovertip(button, "This is tooltip\ntext for button.", hover_delay=0)
def button(self):
window = PopupWindow(self.parent)
window.show()
if __name__ == '__main__':
root = tk.Tk()
App(root)
root.mainloop()
You'll notice that the tooltip for the OK button in the modal window appears behind the window (I'm changing the geometry of the window for otherwise it would be so small that we wouldn't actually notice this).
Of course this becomes a problem in a real window with several widgets the tips for some of them will not be seen at all.
Apparently there are two ways around the problem: one is to delete the line self.gui.wait_visibility() in the __init__ of the PopupWindow class;
the other is to delete the self.gui.grab_set() in the show() method.
With any of these the window is no longer modal (if I get the meaning right: I mean I want the window to stay on top and prevent changes in the parent window while it exists).
The commented line in the show method was my tentative of working around it by defining the tooltip after the grab_set so that it might come on top, but it doesn't work either.
I suppose there must be an way of doing this properly using this class for tooltips.
How can I do it?
Thanks.
I found a solution.
Apparently, someone had the same problem, although with a different class for tooltips.
I refer to this question and answer.
The solution is to add the line tw.wm_attributes("-topmost", 1) somewhere in the showtip method of the TooltipBase class.
I did it as a last line, but I'm not sure it doesn't work some other place; I know it doesn't if immediately after tw.wm_overrideredirect(1).
I have been working on my first GUI in tkinter - I am using Windows. My goal right now is to have buttons that accomplish these goals:
The buttons are highlighted when moused over.
The button remains highlighted if clicked.
Only one button can be "selected" (click-highlighted) at a time.
I initially thought that I had accomplished this! But I realize now that my work is not complete.
Here is what I am seeing:
I mouse over button A. It becomes highlighted! (GOOD)
I click on button A. It stays highlighted! (GOOD)
I mouse over button B. It becomes highlighted! (GOOD)
I click on button B. It stays highlighted! The highlight from A is removed! (GOOD)
I mouse over button A. It does not highlight. (BAD)
I am calling the default_coloring class function on button A when I click on button B. However, this appears to turn off the highlighting functions of button A, and the button no longer functions correctly according to the three rules I listed at the top.
How do I ensure that the buttons continue to function normally, even after the command is called? Am I approaching this the wrong way?
import tkinter as tk
blue = '#0000BB'
white = '#FFFFFF'
class HoverButton(tk.Button):
def __init__(self, master, position = None, **kw):
tk.Button.__init__(self,master=master,**kw)
self.defaultBackground = self["background"]
self.defaultForeground = self["foreground"]
self.bind("<Enter>", self.on_enter)
self.bind("<Leave>", self.on_leave)
self.bind("<Button-1>", self.hover_click)
self.state = 0
self.position = position
def on_enter(self, e):
if self.state == 0:
self['background'] = self['activebackground']
self['foreground'] = self['activeforeground']
def on_leave(self, e):
if self.state == 2:
self.state = 0
if self.state == 0:
self['background'] = self.defaultBackground
self['foreground'] = self.defaultForeground
def hover_click(self, e):
self.state += 1
self.state = self.state % 3
if self.state == 2:
self['background'] = self.defaultBackground
self['foreground'] = self.defaultForeground
def default_coloring(self):
self['background'] = self.defaultBackground
self['foreground'] = self.defaultForeground
class AddOnFrame(tk.Frame):
def __init__(self, master):
self.selectedbutton = None
super().__init__(master)
games = ['A','B','C']
self.objs = list()
self['bg'] = blue
for i in range(3):
self.objs.append(HoverButton(self,position = i, text = games[i].upper(), activebackground = white,activeforeground = blue,fg = white, bg = blue, borderwidth=0, relief = 'flat', highlightbackground = white))
self.objs[i]['command'] = lambda c=i: self._hover_button_clicked(self.objs[c])
self.objs[i].grid(row = i, column = 0, sticky = tk.W + tk.E)
self.blanklabel = tk.Label(self, text = '', background = white)
self.blanklabel.grid(row = 0, column = 1,rowspan = 10, sticky = tk.N + tk.E + tk.W + tk.S)
self.grid_columnconfigure(1, weight=1, minsize=10)
self.grid_columnconfigure(2, weight=1, minsize=500)
self.grid_columnconfigure(3, weight=1, minsize=500)
self.grid_columnconfigure(4, weight=1, minsize=500)
self.pack(expand = True)
def _hover_button_clicked(self, HoverButton):
self.lastbutton = self.selectedbutton
if self.lastbutton != None:
self.objs[self.lastbutton].default_coloring()
self.selectedbutton = HoverButton.position
window = tk.Tk()
window.geometry('1750x950')
window['bg'] = blue
window.title('Testing')
lf = AddOnFrame(window)
lf['bg'] = blue
window.mainloop()
I think I found the main source of the problem. When another button is clicked, you restore color of the last clicked button, but you do not reset its state. Change your default_coloring function to:
def default_coloring(self):
self.state = 0
self['background'] = self.defaultBackground
self['foreground'] = self.defaultForeground
But you should also prevent default_coloring if same button is pressed again:
def _hover_button_clicked(self, HoverButton):
self.lastbutton = self.selectedbutton
if (self.lastbutton != None) and (self.lastbutton != HoverButton.position):
self.objs[self.lastbutton].default_coloring()
self.selectedbutton = HoverButton.position
After cursory inspection, this sequence seems to be the problem:
When a button is clicked, the AddOnFrame._hover_button_clicked
method is invoked.
AddOnFrame.selectedbutton is initially None, which means the
if-statement in AddOnFrame._hover_button_clicked will not be
executed the first time. This is why the buttons seem to work the
first time you click them, but not after that.
However, the next time it is invoked (the next time a button is
pressed), AddOnFrame.selectedbutton is not None, and will never
be None again, meaning that from now on, every click will result in
a call to that HoverButton's default_coloring method.
default_coloring is invoked as soon as a button is clicked, which
results in a quick flash from the active color to the default color,
and the button does not stay highlighted.
The quick fix:
Basically, don't do the default_coloring stuff. It seems to be hurting you more than it's helping. Not really sure why you're doing it in the first place (all that stuff with setting the command, the lambda, the whole _hover_button_clicked method) since the buttons seem to be setting their colors back to the default just fine when on_leave or hover_click are invoked. You can fix your problem by changing the body of your HoverButton.default_coloring function to this:
def default_coloring(self):
return
The real fix would be some restructuring of your code.
EDIT I'm offering this to help you simplify things:
import tkinter as tk
colors = {
"white": "#FFFFFF",
"blue": "#0000BB"
}
class HoverButton(tk.Button):
def __init__(self, *args, **kwargs):
tk.Button.__init__(self, *args, **kwargs)
self.is_selected = False
self.is_highlighted = False
self["borderwidth"] = 0
self["relief"] = tk.FLAT
self["font"] = ("United Sans Cd Bk", 30)
self["activeforeground"] = colors["blue"]
self["activebackground"] = colors["white"]
self["highlightbackground"] = colors["white"]
self.recolor()
self.bind("<Enter>", self.on_enter)
self.bind("<Leave>", self.on_leave)
self.bind("<Button-1>", self.on_click)
def recolor(self):
self["background"] = [colors["blue"], colors["white"]][self.is_highlighted]
self["foreground"] = [colors["white"], colors["blue"]][self.is_highlighted]
def on_enter(self, *args):
self.is_highlighted = True
self.recolor()
def on_leave(self, *args):
if self.is_selected:
return
self.is_highlighted = False
self.recolor()
def on_click(self, *args):
self.is_selected = not self.is_selected
class Application(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.title("Window")
self.geometry("256x256")
self.resizable(width=False, height=False)
self["background"] = colors["blue"]
button_labels = ["A", "B", "C"]
self.buttons = []
for row, button_label in enumerate(button_labels):
button = HoverButton(text=button_label)
button.grid(row=row, column=0, sticky=tk.W)
self.buttons.append(button)
def main():
application = Application()
application.mainloop()
return 0
if __name__ == "__main__":
import sys
sys.exit(main())
I have a QTreeView displaying data from a QStandardItemModel. One of the columns of the tree is displayed with a delegate that lets the user edit and display rich text. Below is a SSCCE that limits the editing to bold (with keyboard shortcut).
When the user is editing one of the items, how can I set it up so that in addition to toggling boldness with keyboard shortcut (CTRL-B), the user can also toggle it using the toolbar icon?
Thus far, the keyboard shortcut works great (you can double click, edit text, and CTRL-B will toggle bold). However, I haven't figured out how to connect the bold button in the toolbar to the appropriate slot:
self.boldTextAction.triggered.connect(self.emboldenText)
where I have this just sitting there doing nothing:
def emboldenText(self):
print "Make selected text bold...How do I do this?"
Things would be easy if the main window's central widget was the text editor: I could directly invoke the text editor's toggle bold method. Unfortunately, the text editor is only generated transiently by the tree view's delegate when the user double-clicks to start editing the tree.
That is, we have this complicated relationship:
QMainWindow -> QTreeView -> Delegate.CreateEditor ->
QTextEdit.toggleBold()
How do I access toggleBold() from within the main window for use by the toolbar action, especially given that the editor is only created temporarily when opened by the user?
I realize this may not be a PySide/Qt question as much as a Python/OOP question, so I've included additional potentially relevant tags. Any help with improving my word choice/jargon would be appreciated too.
SSCCE
#!/usr/bin/env python
import platform
import sys
from PySide import QtGui, QtCore
class MainTree(QtGui.QMainWindow):
def __init__(self, tree, parent = None):
QtGui.QMainWindow.__init__(self)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.setCentralWidget(tree)
self.createStatusBar()
self.createBoldAction()
self.createToolbar()
self.tree = tree
#self.htmlDelegate = self.tree.itemDelegateForColumn(1)
def createStatusBar(self):
self.status = self.statusBar()
self.status.setSizeGripEnabled(False)
self.status.showMessage("In editor, keyboard to toggle bold")
def createToolbar(self):
self.textToolbar = self.addToolBar("Text actions")
self.textToolbar.addAction(self.boldTextAction)
def createBoldAction(self):
self.boldTextAction = QtGui.QAction("Bold", self)
self.boldTextAction.setIcon(QtGui.QIcon("boldText.png"))
self.boldTextAction.triggered.connect(self.emboldenText)
self.boldTextAction.setStatusTip("Make selected text bold")
def emboldenText(self):
print "Make selected text bold...How do I do this? It's stuck in RichTextLineEdit"
class HtmlTree(QtGui.QTreeView):
def __init__(self, parent = None):
QtGui.QTreeView.__init__(self)
model = QtGui.QStandardItemModel()
model.setHorizontalHeaderLabels(['Task', 'Priority'])
rootItem = model.invisibleRootItem()
item0 = [QtGui.QStandardItem('Sneeze'), QtGui.QStandardItem('Low')]
item00 = [QtGui.QStandardItem('Tickle nose'), QtGui.QStandardItem('Low')]
item1 = [QtGui.QStandardItem('Get a job'), QtGui.QStandardItem('<b>High</b>')]
item01 = [QtGui.QStandardItem('Call temp agency'), QtGui.QStandardItem('<b>Extremely</b> <i>high</i>')]
rootItem.appendRow(item0)
item0[0].appendRow(item00)
rootItem.appendRow(item1)
item1[0].appendRow(item01)
self.setModel(model)
self.expandAll()
self.resizeColumnToContents(0)
self.setToolTip("Use keyboard to toggle bold")
self.setItemDelegate(HtmlPainter(self))
class HtmlPainter(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
def paint(self, painter, option, index):
if index.column() == 1:
text = index.model().data(index) #default role is display (for edit consider fixing Valign prob)
palette = QtGui.QApplication.palette()
document = QtGui.QTextDocument()
document.setDefaultFont(option.font)
#Set text (color depends on whether selected)
if option.state & QtGui.QStyle.State_Selected:
displayString = "<font color={0}>{1}</font>".format(palette.highlightedText().color().name(), text)
document.setHtml(displayString)
else:
document.setHtml(text)
#Set background color
bgColor = palette.highlight().color() if (option.state & QtGui.QStyle.State_Selected)\
else palette.base().color()
painter.save()
painter.fillRect(option.rect, bgColor)
document.setTextWidth(option.rect.width())
offset_y = (option.rect.height() - document.size().height())/2
painter.translate(option.rect.x(), option.rect.y() + offset_y)
document.drawContents(painter)
painter.restore()
else:
QtGui.QStyledItemDelegate.paint(self, painter, option, index)
def sizeHint(self, option, index):
fm = option.fontMetrics
if index.column() == 1:
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setDefaultFont(option.font)
document.setHtml(text)
return QtCore.QSize(document.idealWidth() + 5, fm.height())
return QtGui.QStyledItemDelegate.sizeHint(self, option, index)
def createEditor(self, parent, option, index):
if index.column() == 1:
editor = RichTextLineEdit(parent)
editor.returnPressed.connect(self.commitAndCloseEditor)
return editor
else:
return QtGui.QStyledItemDelegate.createEditor(self, parent, option,
index)
def commitAndCloseEditor(self):
editor = self.sender()
if isinstance(editor, (QtGui.QTextEdit, QtGui.QLineEdit)):
self.commitData.emit(editor)
self.closeEditor.emit(editor, QtGui.QAbstractItemDelegate.NoHint)
class RichTextLineEdit(QtGui.QTextEdit):
returnPressed = QtCore.Signal()
def __init__(self, parent=None):
QtGui.QTextEdit.__init__(self, parent)
self.setLineWrapMode(QtGui.QTextEdit.NoWrap)
self.setTabChangesFocus(True)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
fontMetrics = QtGui.QFontMetrics(self.font())
h = int(fontMetrics.height() * (1.4 if platform.system() == "Windows"
else 1.2))
self.setMinimumHeight(h)
self.setMaximumHeight(int(h * 1.2))
self.setToolTip("Press <b>Ctrl+b</b> to toggle bold")
def toggleBold(self):
self.setFontWeight(QtGui.QFont.Normal
if self.fontWeight() > QtGui.QFont.Normal else QtGui.QFont.Bold)
def sizeHint(self):
return QtCore.QSize(self.document().idealWidth() + 5,
self.maximumHeight())
def minimumSizeHint(self):
fm = QtGui.QFontMetrics(self.font())
return QtCore.QSize(fm.width("WWWW"), self.minimumHeight())
def keyPressEvent(self, event):
'''This just handles all keyboard shortcuts, and stops retun from returning'''
if event.modifiers() & QtCore.Qt.ControlModifier:
handled = False
if event.key() == QtCore.Qt.Key_B:
self.toggleBold()
handled = True
if handled:
event.accept()
return
if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
self.returnPressed.emit()
event.accept()
else:
QtGui.QTextEdit.keyPressEvent(self, event)
def main():
app = QtGui.QApplication(sys.argv)
myTree = HtmlTree()
#myTree.show()
myMainTree = MainTree(myTree)
myMainTree.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Note for those that want the Full Tree Experience, with the button in the toolbar, here it is you can put it in the same folder as the script (change the name to boldText.png:
I think from a design point of view the top window is a sort of global. You have already described a behaviour which is treating it in that way and (as ekhumoro has said) that pretty much requires you to provide access to that top window to the editor.
One very simple way to do that is to call parent.window() in the createEditor method. Maybe something like:
parent.window().boldTextAction.triggered.connect(editor.toggleBold)
That seems to work for me.
i'm new on Tkinter and i'm trying to make an animated button.
I'm using the enter-leave events but the click on button it's not responding very well.
My code is:
imagePath = "Resources/"
imagelist = ["boton_1.gif","boton_2.gif","boton_3.gif","boton_4.gif","boton_5.gif","boton_6.gif",
"boton_7.gif","boton_8.gif","boton_9.gif","boton_10.gif","boton_11.gif","boton_12.gif",
"boton_13.gif","boton_14.gif","boton_15.gif","boton_16.gif"]
giflist = []
for imagefile in imagelist:
photo = PhotoImage(file=imagePath+imagefile)
giflist.append(photo)
self.photo=giflist[0]
button = Button(buttonFrame, image=self.photo,background='orange',activebackground='lightsalmon',
command=lambda: controller.show_frame(ListPlayerPage))
button.pack(pady=5)
def enter(event):
self.clickOnButton1 = True
for i in range(1,8):
button.config(image=giflist[i])
button.update()
time.sleep(0.1)
if self.clickOnButton1 == False:
break
while (self.clickOnButton1):
for i in range (9,15):
button.config(image=giflist[i])
button.update()
time.sleep(0.08)
if self.clickOnButton1 == False:
break
def leave(event):
self.clickOnButton1 = False
button.config(image=self.photo)
button.update()
button.bind("<Enter>",enter)
button.bind("<Leave>",leave)
Thanks!!
Part of the problem is definitely related to the fact you're calling sleep. As a good rule of thumb you should never call sleep in the main thread of a GUI. It prevents the GUI from processing all events, including screen refreshes.
Generally speaking, you should also avoid calling update. It can result in nested event loops, if during the processing of update you end up calling a method that again calls update.
Here's a really simple example of solution that creates a button that can be animated. It uses after to iterate over a list of text strings, one new string every half second. This example will animate forever, but you can easily have it show each item only once. This modifies the text to make the example shorter, but you can easily modify it to change images instead of text.
import Tkinter as tk # use tkinter for python 3.x
class AnimatedButton(tk.Button):
def __init__(self, *args, **kwargs):
tk.Button.__init__(self, *args, **kwargs)
self._job = None
def cancel_animation(self):
if self._job is not None:
self.after_cancel(self._job)
self._job = None
def animate(self, textlist):
text = textlist.pop(0)
textlist.append(text)
self.configure(text=text)
self._job = self.after(500, self.animate, textlist)
You use it like any other Button, but you can call animate to start animation and cancel_animate to cancel it:
button = AnimatedButton(root, width=10)
data = ["one","two","three","four","five","six"]
button.bind("<Enter>", lambda event: button.animate(data))
button.bind("<Leave>", lambda event: button.cancel_animation())
I followed the Bryan Oakley example and found a nice solution!
First of all, this is an animated button with a bit complex animation. I have 16 images. The firts one is the base image. Then i have eight images that are the first part of the animation. The rest of the images are the loop part of the animation.
When you put the mouse over the button, the animation starts.
Here is the code!:
import Tkinter as tk # use tkinter for python 3.x
root = tk.Tk()
root.geometry("300x200")
class AnimatedButton(tk.Button):
def __init__(self, *args, **kwargs):
tk.Button.__init__(self, *args, **kwargs)
self._job = None
self.i = 1
def cancel_animation(self,image):
self.configure(image=image)
self.i = 1
if self._job is not None:
self.after_cancel(self._job)
self._job = None
def animate(self, imagelist):
image = imagelist[self.i]
self.i+=1
if self.i == (len(imagelist)-1):
self.i = 9
self.configure(image=image)
self._job = self.after(80, self.animate, imagelist)
imagePath = "Resources/"
imagelist = ["boton_1.gif","boton_2.gif","boton_3.gif","boton_4.gif","boton_5.gif","boton_6.gif",
"boton_7.gif","boton_8.gif","boton_9.gif","boton_10.gif","boton_11.gif","boton_12.gif",
"boton_13.gif","boton_14.gif","boton_15.gif","boton_16.gif"]
giflist = []
for imagefile in imagelist:
photo = tk.PhotoImage(file=imagePath+imagefile)
giflist.append(photo)
image = giflist[0]
button = AnimatedButton(root,image = image)
button.bind("<Enter>", lambda event: button.animate(giflist))
button.bind("<Leave>", lambda event: button.cancel_animation(image))
button.pack()
root.mainloop()
Thank's!!!
I've been using Tkinter and Tix to write a small program.
I'm at a point where I need a tree view with checkboxes (checkbuttons) so I can select items from the tree view.
Is there an easy way to do this?
I've been looking at ttk.Treeview () and it looks easy to get the tree view but is there a way to insert a checkbutton to the view?
A simple code snippet would be really appreciated.
I'm not limited to ttk. Anything will do; as long as I have an example or good docs I can make it work
import Tix
class View(object):
def __init__(self, root):
self.root = root
self.makeCheckList()
def makeCheckList(self):
self.cl = Tix.CheckList(self.root, browsecmd=self.selectItem)
self.cl.pack()
self.cl.hlist.add("CL1", text="checklist1")
self.cl.hlist.add("CL1.Item1", text="subitem1")
self.cl.hlist.add("CL2", text="checklist2")
self.cl.hlist.add("CL2.Item1", text="subitem1")
self.cl.setstatus("CL2", "on")
self.cl.setstatus("CL2.Item1", "on")
self.cl.setstatus("CL1", "off")
self.cl.setstatus("CL1.Item1", "off")
self.cl.autosetmode()
def selectItem(self, item):
print item, self.cl.getstatus(item)
def main():
root = Tix.Tk()
view = View(root)
root.update()
root.mainloop()
if __name__ == '__main__':
main()
I made a treeview class with checkboxes inheriting ttk.Treeview, but the checkboxes are not ttk.Checkbutton but images of checked, unchecked and tristate checkboxes.
import tkinter as tk
import tkinter.ttk as ttk
class CheckboxTreeview(ttk.Treeview):
"""
Treeview widget with checkboxes left of each item.
The checkboxes are done via the image attribute of the item, so to keep
the checkbox, you cannot add an image to the item.
"""
def __init__(self, master=None, **kw):
ttk.Treeview.__init__(self, master, **kw)
# checkboxes are implemented with pictures
self.im_checked = tk.PhotoImage(file='checked.png')
self.im_unchecked = tk.PhotoImage(file='unchecked.png')
self.im_tristate = tk.PhotoImage(file='tristate.png')
self.tag_configure("unchecked", image=self.im_unchecked)
self.tag_configure("tristate", image=self.im_tristate)
self.tag_configure("checked", image=self.im_checked)
# check / uncheck boxes on click
self.bind("<Button-1>", self.box_click, True)
def insert(self, parent, index, iid=None, **kw):
""" same method as for standard treeview but add the tag 'unchecked'
automatically if no tag among ('checked', 'unchecked', 'tristate')
is given """
if not "tags" in kw:
kw["tags"] = ("unchecked",)
elif not ("unchecked" in kw["tags"] or "checked" in kw["tags"]
or "tristate" in kw["tags"]):
kw["tags"] = ("unchecked",)
ttk.Treeview.insert(self, parent, index, iid, **kw)
def check_descendant(self, item):
""" check the boxes of item's descendants """
children = self.get_children(item)
for iid in children:
self.item(iid, tags=("checked",))
self.check_descendant(iid)
def check_ancestor(self, item):
""" check the box of item and change the state of the boxes of item's
ancestors accordingly """
self.item(item, tags=("checked",))
parent = self.parent(item)
if parent:
children = self.get_children(parent)
b = ["checked" in self.item(c, "tags") for c in children]
if False in b:
# at least one box is not checked and item's box is checked
self.tristate_parent(parent)
else:
# all boxes of the children are checked
self.check_ancestor(parent)
def tristate_parent(self, item):
""" put the box of item in tristate and change the state of the boxes of
item's ancestors accordingly """
self.item(item, tags=("tristate",))
parent = self.parent(item)
if parent:
self.tristate_parent(parent)
def uncheck_descendant(self, item):
""" uncheck the boxes of item's descendant """
children = self.get_children(item)
for iid in children:
self.item(iid, tags=("unchecked",))
self.uncheck_descendant(iid)
def uncheck_ancestor(self, item):
""" uncheck the box of item and change the state of the boxes of item's
ancestors accordingly """
self.item(item, tags=("unchecked",))
parent = self.parent(item)
if parent:
children = self.get_children(parent)
b = ["unchecked" in self.item(c, "tags") for c in children]
if False in b:
# at least one box is checked and item's box is unchecked
self.tristate_parent(parent)
else:
# no box is checked
self.uncheck_ancestor(parent)
def box_click(self, event):
""" check or uncheck box when clicked """
x, y, widget = event.x, event.y, event.widget
elem = widget.identify("element", x, y)
if "image" in elem:
# a box was clicked
item = self.identify_row(y)
tags = self.item(item, "tags")
if ("unchecked" in tags) or ("tristate" in tags):
self.check_ancestor(item)
self.check_descendant(item)
else:
self.uncheck_descendant(item)
self.uncheck_ancestor(item)
if __name__ == '__main__':
root = tk.Tk()
t = CheckboxTreeview(root, show="tree")
t.pack()
t.insert("", 0, "1", text="1")
t.insert("1", "end", "11", text="1")
t.insert("1", "end", "12", text="2")
t.insert("12", "end", "121", text="1")
t.insert("12", "end", "122", text="2")
t.insert("122", "end", "1221", text="1")
t.insert("1", "end", "13", text="3")
t.insert("13", "end", "131", text="1")
root.mainloop()
An improved version of CheckboxTreeview is available in the ttkwidgets module.
If you can use Tix, go with #Brandon's solution. If you are stuck with Ttk (as I am), here is an solution based on #j_4231's idea. Rather than using an image to represent the checkbox, we can use two characters provided by Unicode:
'BALLOT BOX' (U+2610) : ☐
'BALLOT BOX WITH X (U+2612)' : ☒.
Those character are located after the item name and are used to check the current state: treeview.item(iid, "text")[-1] is either ☐ or ☒. We can update the item name when the text is clicked.
The class TtkCheckList inherits ttk.Treeview, hence the usual parameters/methods of Treeview can be used.
import tkinter as tk
from tkinter import ttk
BALLOT_BOX = "\u2610"
BALLOT_BOX_WITH_X = "\u2612"
class TtkCheckList(ttk.Treeview):
def __init__(self, master=None, width=200, clicked=None, separator='.',
unchecked=BALLOT_BOX, checked=BALLOT_BOX_WITH_X, **kwargs):
"""
:param width: the width of the check list
:param clicked: the optional function if a checkbox is clicked. Takes a
`iid` parameter.
:param separator: the item separator (default is `'.'`)
:param unchecked: the character for an unchecked box (default is
"\u2610")
:param unchecked: the character for a checked box (default is "\u2612")
Other parameters are passed to the `TreeView`.
"""
if "selectmode" not in kwargs:
kwargs["selectmode"] = "none"
if "show" not in kwargs:
kwargs["show"] = "tree"
ttk.Treeview.__init__(self, master, **kwargs)
self._separator = separator
self._unchecked = unchecked
self._checked = checked
self._clicked = self.toggle if clicked is None else clicked
self.column('#0', width=width, stretch=tk.YES)
self.bind("<Button-1>", self._item_click, True)
def _item_click(self, event):
assert event.widget == self
x, y = event.x, event.y
element = self.identify("element", x, y)
if element == "text":
iid = self.identify_row(y)
self._clicked(iid)
def add_item(self, item):
"""
Add an item to the checklist. The item is the list of nodes separated
by dots: `Item.SubItem.SubSubItem`. **This item is used as `iid` at
the underlying `Treeview` level.**
"""
try:
parent_iid, text = item.rsplit(self._separator, maxsplit=1)
except ValueError:
parent_iid, text = "", item
self.insert(parent_iid, index='end', iid=item,
text=text+" "+self._unchecked, open=True)
def toggle(self, iid):
"""
Toggle the checkbox `iid`
"""
text = self.item(iid, "text")
checked = text[-1] == self._checked
status = self._unchecked if checked else self._checked
self.item(iid, text=text[:-1] + status)
def checked(self, iid):
"""
Return True if checkbox `iid` is checked
"""
text = self.item(iid, "text")
return text[-1] == self._checked
def check(self, iid):
"""
Check the checkbox `iid`
"""
text = self.item(iid, "text")
if text[-1] == self._unchecked:
self.item(iid, text=text[:-1] + self._checked)
def uncheck(self, iid):
"""
Uncheck the checkbox `iid`
"""
text = self.item(iid, "text")
if text[-1] == self._checked:
self.item(iid, text=text[:-1] + self._unchecked)
Here is an example:
items = [
'Item',
'Item.SubItem1',
'Item.SubItem2',
'Item.SubItem2.SubSubItem1',
'Item.SubItem2.SubSubItem2',
'Item.SubItem2.SubSubItem3',
'Item.SubItem3',
'Item.SubItem3.SubSubItem1',
'Item.SubItem4'
]
root = tk.Tk()
root.title('Test')
root.geometry('400x300')
check_list = TtkCheckList(root, height=len(items))
for item in items:
check_list.add_item(item)
check_list.pack()
root.mainloop()
You can use the clicked parameter to define a new behavior when an item is
clicked. For instance:
def obey_ancestor(iid):
"""
If the status of an item is toggled, the status of all its descendants
is also set to the new status.
"""
set_status = check_list.uncheck if check_list.checked(iid) else check_list.check
stack = [iid]
while stack:
iid = stack.pop()
set_status(iid)
stack.extend(check_list.get_children(iid))
And:
check_list = TtkCheckList(root, height=len(items),
clicked=obey_ancestor)
I would add to jferard's great answer that if you want to have a table of values rather than a tree structure change the following:
In the init add:
self.column('#1', width=width, stretch=tk.YES)
for each column you want.
add_item should be:
def add_item(self, item):
"""
Add an item to the checklist. The item is the list of nodes separated
by dots: `Item.SubItem.SubSubItem`. **This item is used as `iid` at
the underlying `Treeview` level.**
"""
# try:
# parent_iid, text = item.rsplit(self._separator, maxsplit=1)
# except ValueError:
# parent_iid, text = "", item
# self.insert(parent_iid, index='end', iid=item, text=text+" "+self._unchecked, open=True)
self.insert('', index='end', iid=item, values = item, text=self._unchecked, open=True)
Change the example as such:
cols = ['One', 'Two']
items = [('A', '1',),('B','2')]
check_list = TtkCheckList(root, columns = cols, height=len(items))