I am trying to make a toggle button class by deriving from the tkinter.Button object. To that end, I am using this StackOverflow answer and these code examples.
The problem is that I get my desired toggle behavior from the button only after I click it twice; the first two clicks, it does not enact the self.config(relief="sunken"). I tried using the command keyword argument sample from this answer and that works from the start.
import tkinter as tk
class ToggleButton(tk.Button):
def __init__(self, parent=None, toggle_text="Toggled", toggle_bg_color="green", **kwargs):
tk.Button.__init__(self, parent, **kwargs)
self.toggled = False
self.default_bg_color = self['bg']
self.default_text = self["text"]
self.toggle_bg_color = toggle_bg_color
self.toggle_text = toggle_text
self.bind("<Button-1>", self.toggle, add="+")
def toggle(self, *args):
if self["relief"] == "sunken":
self["bg"] = self.default_bg_color
self["text"] = self.default_text
self.config(relief="raised")
# self["relief"] = "raised"
self.toggled = False
else:
self["bg"] = self.toggle_bg_color
self["text"] = self.toggle_text
# self["relief"] = "sunken"
self.config(relief="sunken")
self.toggled = True
def button_placeholder():
print("TO BE IMPLEMENTED")
root = tk.Tk()
button = ToggleButton(parent=root,
toggle_text="ON", toggle_bg_color="green",
text="OFF", command=button_placeholder)
button.pack()
root.mainloop()
Here are screenshots of the behavior of the buttons after numerous clicks
After the first two clicks on the button, the expected behavior occurs. However, if the user focuses on another window (for instance by minimizing the tkinter window) and then back, again the first two clicks do not cause the desired behavior.
Can some explain this? If not, can someone provide a solution where I can have consistent behavior on toggling my button?
Information about my system
Windows 10; 64 bit
Python 3.7.3 (64 bit)
Tkinter 8.6
The problem you seem to have is that the bg parameter is not defined when you first create the button; it only gets a value assigned upon the first button press.
Then, the logic to toggle is hard to follow: you have a self.toggled boolean, yet you are testing if the button is sunken or not to differentiate between states...
I reorganized the logic to make it easier to follow; after all, toggle is a binary change from one state to another. I therefore placed the definition of the ON and OFF states in the body of the class (into two class dictionaries), and the code swaps the two configs upon toggling.
On Windows:
import tkinter as tk
class ToggleButton(tk.Button):
ON_config = {'bg': 'green',
'text': 'button is ON',
'relief': 'sunken',
}
OFF_config = {'bg': 'white',
'text': 'button is OFF',
'relief': 'raised',
}
def __init__(self, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.toggled = False
self.config = self.OFF_config
self.config_button()
self.bind("<Button-1>", self.toggle)
def toggle(self, *args):
if self.toggled: # True = ON --> toggle to OFF
self.config = self.OFF_config
else:
self.config = self.ON_config
self.toggled = not self.toggled
return self.config_button()
def config_button(self):
self['bg'] = self.config['bg']
self['text'] = self.config['text']
self['relief'] = self.config['relief']
return "break"
def __str__(self):
return f"{self['text']}, {self['bg']}, {self['relief']}"
def button_placeholder():
print('toggling now!')
if __name__ == '__main__':
root = tk.Tk()
button = ToggleButton(root)
button.pack()
root.mainloop()
On OSX:
Where the buttons aspect is fixed, using a tk.Label can mimic the desired behavior:
import tkinter as tk
class ToggleButtonLBL(tk.Label):
ON_config = {'bg': 'green',
'text': 'button is ON',
'relief': 'sunken',
}
OFF_config = {'bg': 'white',
'text': 'button is OFF',
'relief': 'raised',
}
def __init__(self, parent, *args, command=None, **kwargs):
super().__init__(parent, *args, **kwargs)
self.toggled = False
self.config = self.OFF_config
self.config_button()
self.bind("<Button-1>", self._toggle_helper)
self.bind("<ButtonRelease-1>", self._toggle)
self.command = command
def _toggle_helper(self, *args):
return 'break'
def _toggle(self, dummy_event):
self.toggle()
self.cmd()
def toggle(self, *args):
if self.toggled: # True = ON --> toggle to OFF
self.config = self.OFF_config
else:
self.config = self.ON_config
self.toggled = not self.toggled
self.config_button()
return 'break'
def config_button(self):
self['bg'] = self.config['bg']
self['text'] = self.config['text']
self['relief'] = self.config['relief']
return "break"
def __str__(self):
return f"{self['text']}, {self['bg']}, {self['relief']}"
def cmd(self):
self.command()
def button_placeholder():
print('toggling now!')
if __name__ == '__main__':
root = tk.Tk()
button = ToggleButtonLBL(root, command=button_placeholder)
button.pack()
root.mainloop()
Related
I'm developing a GUI where you can connect nodes between them except in a few special cases. This implementation works perfectly fine most of the time, but after some testing i found that, when I connect one QGraphicsPixmapItem with another through a QGraphicsLineItem, and the user opens the contextual menu before completing the link, the line get stuck, and it cannot be deleted.
The process to link two elements is to first press the element, then keep pressing while moving the line and releasing when the pointer is over the other element. This is achieved using mousePressEvent, mouseMoveEvent and mouseReleaseEvent, respectively.
This code is an example:
#!/usr/bin/env python3
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
class Ellipse(QGraphicsEllipseItem):
def __init__(self, x, y):
super(Ellipse, self).__init__(x, y, 30, 30)
self.setBrush(QBrush(Qt.darkBlue))
self.setFlag(QGraphicsItem.ItemIsMovable)
self.setZValue(100)
def contextMenuEvent(self, event):
menu = QMenu()
first_action = QAction("First action")
second_action = QAction("Second action")
menu.addAction(first_action)
menu.addAction(second_action)
action = menu.exec(event.screenPos())
class Link(QGraphicsLineItem):
def __init__(self, x, y):
super(Link, self).__init__(x, y, x, y)
self.pen_ = QPen()
self.pen_.setWidth(2)
self.pen_.setColor(Qt.red)
self.setPen(self.pen_)
def updateEndPoint(self, x2, y2):
line = self.line()
self.setLine(line.x1(), line.y1(), x2, y2)
class Scene(QGraphicsScene):
def __init__(self):
super(Scene, self).__init__()
self.link = None
self.link_original_node = None
self.addItem(Ellipse(200, 400))
self.addItem(Ellipse(400, 400))
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
item = self.itemAt(event.scenePos(), QTransform())
if item is not None:
self.link_original_node = item
offset = item.boundingRect().center()
self.link = Link(item.scenePos().x() + offset.x(), item.scenePos().y() + offset.y())
self.addItem(self.link)
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
if self.link is not None:
self.link.updateEndPoint(event.scenePos().x(), event.scenePos().y())
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
if self.link is not None:
item = self.itemAt(event.scenePos(), QTransform())
if isinstance(item, (Ellipse, Link)):
self.removeItem(self.link)
self.link_original_node = None
self.link = None
class MainWindow(QMainWindow):
def __init__(self):
super(QMainWindow, self).__init__()
self.scene = Scene()
self.canvas = QGraphicsView()
self.canvas.setScene(self.scene)
self.setCentralWidget(self.canvas)
self.setGeometry(500, 200, 1000, 600)
self.setContextMenuPolicy(Qt.NoContextMenu)
app = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(app.exec())
How can I get rid off the line before/after the context menu event? I tried to stop them, but I do not know how.
Assuming that the menu is only triggered from a mouse button press, the solution is to remove any existing link item in the mouseButtonPress too.
def mousePressEvent(self, event):
if self.link is not None:
self.removeItem(self.link)
self.link_original_node = None
self.link = None
# ...
Note that itemAt for very small items is not always reliable, as the item's shape() might be slightly off the mapped mouse position. Since the link would be removed in any case, just do the same in the mouseReleaseEvent():
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
if self.link is not None:
item = self.itemAt(event.scenePos(), QTransform())
if isinstance(item, Ellipse):
# do what you need with the linked ellipses
# note the indentation level
self.removeItem(self.link)
self.link_original_node = None
self.link = None
I am trying to assign some behaviors to the buttons, some I have already achieved like:
Change the color of the button if the mouse is positioned over it.
Restore the default button color.
Save the last button pressed in green.
Today I realized that when I press a button without releasing the click, and I move the mouse pointer off the button and release the click, it turns green, but without having executed the linked function, I would like the button not to change color . I am trying to eliminate this behavior, but I have no ideas. The code is executable, it works with python 3.7. Thanks.
from tkinter import *
class TypeButton(Button):
def __init__(self, parent, *args, **kwargs):
kwargs = {'font':('Calibri',9,'bold'), 'bg': '#11161d', 'fg':'white',
'width':10, 'bd':0, 'activebackground':'#bdfe04', **kwargs}
super().__init__(parent, *args, **kwargs)
class Example(Frame):
def __init__(self,parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.frame = Frame(self, bg='#11161d')
self.frame .grid(padx=(10,10), pady=(6,6))
self.container1 = None
self.creator_buttons()
#????????????????
self.bind_all("<B1-Motion>", self.callback)
def creator_buttons(self):
mobiles = [['Frog', 'Fox', 'Boomer', 'Ice', 'J.d', 'Grub', 'Lightning', 'Aduka', 'Knight', 'Kalsiddon', 'Mage'],
['Randomizer', 'Jolteon', 'Turtle', 'Armor', 'A.sate', 'Raon', 'Trico', 'Nak', 'Bigfoot', 'Barney', 'Dragon']]
self.mobiles2 = ['Fox','Knight','Jolteon','Barney','Dragon']
self.buttons22 = []
for index1, mobil in enumerate(mobiles):
for index2, texto in enumerate(mobil):
number = 11 if index1 == 1 else 0
btn = B1ButtonCls (self.frame_1, text=texto, command= self.callback)
n1 = 5 if index2 == 0 else 0
n2 = 5 if index2 == 10 else 0
btn .grid(column=index2 , row=index1 , pady=3, padx=(n1,n2))
btn.bind("<Enter>", self.enter_mouse)
btn.bind("<Leave>", self.leave_mouse)
btn.bind("<Button-1>", self.clic_mouse)
if texto in self.mobiles2: btn.config(fg='yellow')
self.buttons22.append(btn)
def enter_mouse(self, event):
widget1 = event.widget
if not widget1 .cget('bg') == '#bdfe04':
widget1 .config(bg="#24364a")
def leave_mouse(self, event):
if not event.widget .cget('bg') == '#bdfe04':
event.widget.config(bg='#11161d')
def clic_mouse(self, event):
widget1 = event.widget
widget1.config(bg='#bdfe04', fg='black')
if self.container1 is not None and self.container1 != widget1:
if self.container1 .cget('text') in self.mobiles2:
self.container1 .config (bg='#11161d', fg='yellow')
else:
self.container1 .config (bg='#11161d', fg='white')
self.container1 = widget1
def callback(self):
print('Closed')
root = Tk()
app = Example(root).pack()
root.mainloop()
You can use the winfo_containing method to know which widget is under the mouse when you release a button. You can then compare the result of that function call to the widget that was clicked on.
Here's an example that displays one of two text messages depending on whether or not you released the mouse button over the same widget that was clicked on.
import tkinter as tk
def _button_press(event):
label.configure(text="")
def _button_release(event):
widget_under_cursor = event.widget.winfo_containing(event.x_root, event.y_root)
if widget_under_cursor == event.widget:
label.configure(text="you released over the button")
else:
label.configure(text="you did not release over the button")
root = tk.Tk()
label = tk.Label(root, width=40)
label.pack(side="top", fill="x")
for i in range(10):
button = tk.Button(root, text=f"Button #{i+1}")
button.pack()
button.bind("<ButtonPress-1>", _button_press)
button.bind("<ButtonRelease-1>", _button_release)
root.mainloop()
The bool is currently updated when I click on and off the checkbox however when I click the button it will always print the default value I define at the start. How can I update a bool that is outside the class, so that when I click the button and call the function, it updates the bool?
Also, how can I call a function like this when clicking the button, that is outside of the class?
Thanks
someBool = False
def someFunction():
print(someBool)
class MyGridLayout(GridLayout):
def __init__(self, **kwargs):
#grid layout constructor
super(MyGridLayout, self).__init__(**kwargs)
#set columns for the layout
self.cols = 2
self.add_widget(Label(text="checkbox"))
self.checkbox= CheckBox(active = False)
self.add_widget(self.checkbox)
#this will bind the label and checkbox
self.checkbox.bind(active = self.checkboxActive)
self.button= Button(text="Button")
self.button.bind(on_press=someFunction)
self.add_widget(self.button)
def checkboxActive(self, checkbox, value):
if value:
someBool = True
else:
someBool = False
class MyApp(App):
def build(self):
return MyGridLayout()
if __name__ == '__main__':
MyApp().run()
If you want to change a value outside a function in python you have to specify its scope. It can be easily done with global variable.
Then for the button you have to pass an anonymous function to kivy on_press method linking it to the function you want. That way the press will trigger the function passed. So we will have to use lambda for this.
Try this code:
someBool = False
def someFunction():
global someBool
print(someBool)
class MyGridLayout(GridLayout):
def __init__(self, **kwargs):
# grid layout constructor
super(MyGridLayout, self).__init__(**kwargs)
# set columns for the layout
self.cols = 2
self.add_widget(Label(text="checkbox"))
self.checkbox = CheckBox(active=False)
self.add_widget(self.checkbox)
# this will bind the label and checkbox
self.checkbox.bind(active=self.checkboxActive)
self.button = Button(text="Button")
self.button.bind(on_press=lambda x: someFunction())
self.add_widget(self.button)
def checkboxActive(self, checkbox, value):
global someBool
if value:
someBool = True
else:
someBool = False
class MyApp(App):
def build(self):
return MyGridLayout()
if __name__ == '__main__':
MyApp().run()
I am making this PDF tool, and I want the buttons to be disabled until a file or files are successfully imported. This is what the app looks like at the launch:
Right after running the callback for the import files button, the active state looks like this:
I want the colors of the buttons to turn maroon instead of the original grey. They only turn back to maroon once you hover the mouse over them. Any thoughts for how to fix this? Here is the callback for the import button:
def import_callback():
no_files_selected = False
global files
files = []
try:
ocr_button['state'] = DISABLED
merge_button['state'] = DISABLED
status_label.pack_forget()
frame.pack_forget()
files = filedialog.askopenfilenames()
for f in files:
name, extension = os.path.splitext(f)
if extension != '.pdf':
raise
if not files:
no_files_selected = True
raise
if frame.winfo_children():
for label in frame.winfo_children():
label.destroy()
make_import_file_labels(files)
frame.pack()
ocr_button['state'] = ACTIVE
merge_button['state'] = ACTIVE
except:
if no_files_selected:
status_label.config(text='No files selected.', fg='blue')
else:
status_label.config(text='Error: One or more files is not a PDF.', fg='red')
status_label.pack(expand='yes')
import_button = Button(root, text='Import Files', width=scaled(20), bg='#5D1725', bd=0, fg='white', relief='groove',
command=import_callback)
import_button.pack(pady=scaled(50))
I know this was asked quite a while ago, so probably already solved for the user. But since I had the exact same problem and do not see the "simplest" answer here, I thought I would post:
Just change the state from "active" to "normal"
ocr_button['state'] = NORMAL
merge_button['state'] = NORMAL
I hope this helps future users!
As I understand you right you want something like:
...
ocr_button['state'] = DISABLED
ocr_button['background'] = '#*disabled background*'
ocr_button.bind('<Enter>', lambda e:ocr_button.configure(background='#...'))
ocr_button.bind('<Leave>', lambda e:ocr_button.configure(background='#...'))
merge_button['state'] = DISABLED
merge_button['background'] = '#*disabled background*'
merge_button.bind('<Enter>', lambda e:ocr_button.configure(background='#...'))
merge_button.bind('<Leave>', lambda e:ocr_button.configure(background='#...'))
...
...
ocr_button['state'] = ACTIVE
ocr_button['background'] = '#*active background*'
ocr_button.unbind('<Enter>')
ocr_button.unbind('<Leave>')
merge_button['state'] = ACTIVE
merge_button['background'] = '#*active background*'
merge_button.unbind('<Enter>')
merge_button.unbind('<Leave>')
...
If there are any errors, since I wrote it out of my mind or something isnt clear, let me know.
Update
the following code reproduces the behavior as you stated. The reason why this happens is how tkinter designed the standart behavior. You will have a better understanding of it if you consider style of ttk widgets. So I would recommand to dont use the automatically design by state rather write a few lines of code to configure your buttons how you like, add and delete the commands and change the background how you like. If you dont want to write this few lines you would be forced to use ttk.Button and map a behavior you do like
import tkinter as tk
root = tk.Tk()
def func_b1():
print('func of b1 is running')
def disable_b1():
b1.configure(bg='grey', command='')
def activate_b1():
b1.configure(bg='red', command=func_b1)
b1 = tk.Button(root,text='B1', bg='red',command=func_b1)
b2 = tk.Button(root,text='disable', command=disable_b1)
b3 = tk.Button(root,text='activate',command=activate_b1)
b1.pack()
b2.pack()
b3.pack()
root.mainloop()
I've wrote this simple app that I think could help all to reproduce the problem.
Notice that the state of the button when you click is Active.
#!/usr/bin/python3
import sys
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
class Main(ttk.Frame):
def __init__(self, parent, *args, **kwargs):
super().__init__()
self.parent = parent
self.init_ui()
def cols_configure(self, w):
w.columnconfigure(0, weight=0, minsize=100)
w.columnconfigure(1, weight=0)
w.rowconfigure(0, weight=0, minsize=50)
w.rowconfigure(1, weight=0,)
def get_init_ui(self, container):
w = ttk.Frame(container, padding=5)
self.cols_configure(w)
w.grid(row=0, column=0, sticky=tk.N+tk.W+tk.S+tk.E)
return w
def init_ui(self):
w = self.get_init_ui(self.parent)
r = 0
c = 0
b = ttk.LabelFrame(self.parent, text="", relief=tk.GROOVE, padding=5)
self.btn_import = tk.Button(b,
text="Import Files",
underline=1,
command = self.on_import,
bg='#5D1725',
bd=0,
fg='white')
self.btn_import.grid(row=r, column=c, sticky=tk.N+tk.W+tk.E,padx=5, pady=5)
self.parent.bind("<Alt-i>", self.switch)
r +=1
self.btn_ocr = tk.Button(b,
text="OCR FIles",
underline=0,
command = self.on_ocr,
bg='#5D1725',
bd=0,
fg='white')
self.btn_ocr["state"] = tk.DISABLED
self.btn_ocr.grid(row=r, column=c, sticky=tk.N+tk.W+tk.E,padx=5, pady=5)
r +=1
self.btn_merge = tk.Button(b,
text="Merge Files",
underline=0,
command = self.on_merge,
bg='#5D1725',
bd=0,
fg='white')
self.btn_merge["state"] = tk.DISABLED
self.btn_merge.grid(row=r, column=c, sticky=tk.N+tk.W+tk.E,padx=5, pady=5)
r +=1
self.btn_reset = tk.Button(b,
text="Reset",
underline=0,
command = self.switch,
bg='#5D1725',
bd=0,
fg='white')
self.btn_reset.grid(row=r, column=c, sticky=tk.N+tk.W+tk.E,padx=5, pady=5)
b.grid(row=0, column=1, sticky=tk.N+tk.W+tk.S+tk.E)
def on_import(self, evt=None):
self.switch()
#simulate some import
self.after(5000, self.switch())
def switch(self,):
state = self.btn_import["state"]
if state == tk.ACTIVE:
self.btn_import["state"] = tk.DISABLED
self.btn_ocr["state"] = tk.NORMAL
self.btn_merge["state"] = tk.NORMAL
else:
self.btn_import["state"] = tk.NORMAL
self.btn_ocr["state"] = tk.DISABLED
self.btn_merge["state"] = tk.DISABLED
def on_ocr(self, evt=None):
state = self.btn_ocr["state"]
print ("ocr button state is {0}".format(state))
def on_merge(self, evt=None):
state = self.btn_merge["state"]
print ("merge button state is {0}".format(state))
def on_close(self, evt=None):
self.parent.on_exit()
class App(tk.Tk):
"""Main Application start here"""
def __init__(self, *args, **kwargs):
super().__init__()
self.protocol("WM_DELETE_WINDOW", self.on_exit)
self.set_style()
self.set_title(kwargs['title'])
Main(self, *args, **kwargs)
def set_style(self):
self.style = ttk.Style()
#('winnative', 'clam', 'alt', 'default', 'classic', 'vista', 'xpnative')
self.style.theme_use("clam")
def set_title(self, title):
s = "{0}".format('Simple App')
self.title(s)
def on_exit(self):
"""Close all"""
if messagebox.askokcancel(self.title(), "Do you want to quit?", parent=self):
self.destroy()
def main():
args = []
for i in sys.argv:
args.append(i)
kwargs = {"style":"clam", "title":"Simple App",}
app = App(*args, **kwargs)
app.mainloop()
if __name__ == '__main__':
main()
from PyQt4 import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.view = View(self)
self.button = QtGui.QPushButton('Clear View', self)
self.button.clicked.connect(self.handleClearView)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.view)
layout.addWidget(self.button)
def handleClearView(self):
self.view.scene().clear()
class DragButton(QtGui.QPushButton):
def mousePressEvent(self, event):
self.__mousePressPos = None
self.__mouseMovePos = None
if event.button() == QtCore.Qt.LeftButton:
self.__mousePressPos = event.globalPos()
self.__mouseMovePos = event.globalPos()
#super(DragButton, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton:
# adjust offset from clicked point to origin of widget
currPos = self.mapToGlobal(self.pos())
globalPos = event.globalPos()
diff = globalPos - self.__mouseMovePos
newPos = self.mapFromGlobal(currPos + diff)
self.move(newPos)
self.__mouseMovePos = globalPos
#super(DragButton, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self.__mousePressPos is not None:
moved = event.globalPos() - self.__mousePressPos
if moved.manhattanLength() > 3:
event.ignore()
return
#super(DragButton, self).mouseReleaseEvent(event)
class View(QtGui.QGraphicsView):
def __init__(self, parent):
QtGui.QGraphicsView.__init__(self, parent)
self.setScene(QtGui.QGraphicsScene(self))
self.setSceneRect(QtCore.QRectF(self.viewport().rect()))
btn1=DragButton('Test1', self)
btn2=DragButton('Test2', self)
def mousePressEvent(self, event):
self._start = event.pos()
def mouseReleaseEvent(self, event):
start = QtCore.QPointF(self.mapToScene(self._start))
end = QtCore.QPointF(self.mapToScene(event.pos()))
self.scene().addItem(
QtGui.QGraphicsLineItem(QtCore.QLineF(start, end)))
for point in (start, end):
text = self.scene().addSimpleText(
'(%d, %d)' % (point.x(), point.y()))
text.setBrush(QtCore.Qt.red)
text.setPos(point)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.resize(640, 480)
window.show()
sys.exit(app.exec_())
Here is my code. There are two movable buttons on the QGraphicsView and I can draw line on the QGraphicsView with mouse dragging. But what I want to do is to draw line between two buttons. For detail, If I right click the btn1(Test1) and then right click the btn2(Test2) , the line would be created between two buttons. I'm struggling this problem for a month. Plz Help!
I am assuming that the line you need to draw between the buttons must also be movable. If not it is just simple you can just use :
lines = QtGui.QPainter()
lines.setPen(self)
lines.drawLine(x1,y1,x2,y2)
So, if the line needs to be movable along with the buttons then first you create a mini widget consisting of Two Buttons and the Line, so you can move the whole widget. This might help!, in that case.