If I have a button that executes a handler in relation to one toplevel window or root - can it modify a widget in another toplevel? What is the standard method of doing this?
I am not experienced in Tkinter, but I am sure its no different than PyQt4. Generally you want the parent child relationship. A parent widget contains children widgets and can modify them. The children should usually not know much about their parents and would communicate via signals or events.
When a handler fires, it should be free to modify the widgets it knows about. That is, if your top level window has 10 widgets, and contains the handler as a method, the method should be allowed to modify the 10 widgets through self attributes.
Something like:
class MyTopWindow:
def __init__(self):
self.widget1 = Widget
self.widget2 = Widget
self.button = Button
#self.button.clicked -> self.handler
def handler(self):
self.widget1.someAttribute = 1
button.click() # -> handler()
You make use of composition to build widgets inside of widgets. What you should avoid is a handler making some reference to a global widget that it really does not own.
Yes, a handler for a button (or any event) can modify any widget in any other toplevel. As long as that handler has a reference to the widget, the handler can modify it. There are no restrictions in that regard.
Related
This should be a no-brainer but I just can't seem to figure it out. If I build a Gtk.PopoverMenu:
menu = Gio.Menu.new()
test = Gio.MenuItem.new(label='test', detailed_action='win.test')
menu.append_item(test)
popup = Gtk.PopoverMenu.new_from_model(menu)
I don't know how to attach it to the parent widget (in my case a ListBox item). The API docs say "Popovers are attached to a parent widget" but there seems to be no relevant method for attaching it to the parent. Trying to popup.popup() results in a warning "Calling gtk_widget_realize() on a widget that isn't inside a toplevel window is not going to work very well. Widgets must be inside a toplevel container before realizing them", and a subsequent segfault.
To set the parent of the PopoverMenu, just use its set_parent() method to set the parent to any widget:
...
popup = Gtk.PopoverMenu.new_from_model(menu)
popup.set_parent(parent_widget)
I'm a little bit confused about the use of the "self" parameter with some widgets like (QLineEdit), indeed when learning to use the QLabel widget, I used to call the class without the self paramater, or when using the QLineEdit widget, the widget wouldn't work without the "self" parameter, here's the code I'm working on :
# Import necessary modules
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton
from PyQt5.QtCore import Qt
class EntryWindow(QWidget): # Inherits QWidget
def __init__(self): # Constructor
super().__init__() # Initializer which calls constructor for QWidget
self.initializeUI() # Call function used to set up window
def initializeUI(self):
"""
Initialize the window and display its contents to the screen
"""
self.setGeometry(400, 300, 400, 200)
self.setWindowTitle('QLineEdit Widget')
self.displayWidgets()
self.show() # Show everything
def displayWidgets(self):
'''
Setup the QLineEdit and other widgets.
'''
# Create name label and line edit widgets
QLabel("Please enter your name below.", self).move(100, 20)
name_label = QLabel("Name:", self)
name_label.move(55, 70)
self.name_entry = QLineEdit(self)
self.name_entry.move(120, 68)
self.name_entry.resize(200, 25) # Change size of entry field
self.name_entry.setAlignment(Qt.AlignLeft) # The default alignment
text_font = self.name_entry.font() # Get font option from the Qlineedit
text_font.setPointSize(12) # Modify font size
#text_font.setBold(True) # Bold
self.name_entry.setFont(text_font) # Apply font
self.clear_button = QPushButton('Clear text', self)
self.clear_button.clicked.connect(self.clearEntries)
self.clear_button.move(120, 130)
self.exit_button = QPushButton("Exit", self)
self.exit_button.clicked.connect(self.exitApplication)
self.exit_button.move(240, 130)
def clearEntries(self):
sender = self.sender()
if sender.text() == 'Clear text':
self.name_entry.clear()
def exitApplication(self):
sender = self.sender()
if sender.text() == "Exit":
self.close() # Close the window
# Run program
if __name__ == '__main__':
app = QApplication(sys.argv)
window = EntryWindow()
sys.exit(app.exec_())
So here is where I'm confused, when using QLabel, I didn't have to put the "self" parameter before, or when using QLineEdit, I had to put "self" otherwise my code wouldn't work :
QLabel("Please enter your name below.", self).move(100, 20)
self.name_entry = QLineEdit(self)
First of all, the problem or difference has nothing to do with the "self", but what it is for, pre-established rules in Qt design.
In Qt there is a hierarchy tree between the QObjects where it is established that the parent QObject manages the memory (the life cycle of its children) so if the parent deleted the children, they will also be deleted. This allows avoiding memory leaks since many QObjects are generally used in many applications.
On the other hand, that concept of kinship is also passed to QWidgets since they are also QObjects, but there is also another characteristic: a QWidget in general will be drawn on top of its parent. So if you want the QLineEdit and QLabel to be part of the window then they must be children of the window so it is necessary that you pass the window object which is "self" as parent.
So when you go to the window (in this case "self") you avoid 2 problems:
That the object has a longer life cycle (the same as the window).
And you make the widget (either QLabel or QLineEdit) be placed on top of the window.
The self name is a Python convention to indicate the instance of an object. When set as argument in an object constructor, it's normally used to give the new object a reference to the one that created it, and that's normally called a "parent". Note that while self is usually used as parent argument, you can set any QWidget instance as parent.
In Qt terms, not only it indicates the roles in a "parentship" of the object structure, but also results in two important aspects:
In the world of QObjects (from which QWidget inherits) this helps for memory management: when a parent is deleted, all its children objects are deleted as well. It's also useful to find child objects (using findChild() or findChildren()) which are direct children or [great, ...] grandchildren of another.
When creating a widget with a QWidget parent, that widget is also only "drawn inside" it: it can only be visible within the boundaries of the parent, not outside it; note that this is usually optional, as most times you'll add a widget to a layout manager (but you didn't), which automatically reparents the widget to the widget that is managed by that layout: if it was a child of another widget, it's removed from that and added to the new one. Also consider that if a widget has no parent and is not added to a layout, it's considered a "top level widget" (a standalone window), and if show() or setVisible(True) is called it will have its own window.
This means that if the widget has a parent and the parent becomes visible, the child will be automatically visible too (unless hide() or setVisible() was explicitly called).
Note that while a QWidget that has no parent is always a top level window, a widger that has a parent could be a top level window, and that happens if it has the QtCore.Qt.Window flag set (using the f keyword in the constructor, or with setWindowFlags()). This is useful when the widget has to be a top level window, but the parentship must still be preserved, not only for memory management (when the parent window is closed and deleted, any child should be deleted as well) but for interaction purposes: for example, with QDialogs (which are standalone windows) it's important to set the parent, so that they can be modal to that parent, meaning that no keyboard/mouse interaction can happen on the parent until the dialog is closed.
Finally, it's important to remember that creating widgets "on the fly" in python has a (sometimes perceived incoherent) result: if no python reference is given to the widget but the widget has a parent, it won't garbage collected.
Consider the following:
def test1(self):
someDialog = QtWidgets.QDialog()
someDialog.show()
This will probably show a window just for an instant, and then it will disappear instantly.
def test2(self):
someDialog = QtWidgets.QDialog(self)
someDialog.show()
In this case, the dialog will not disappear, that's because it has a parent, PyQt knows it, and it will not try to delete it.
Even this is actually valid (but, no, don't do it):
def test2b(self):
QtWidgets.QDialog(self).show()
Similar result, but only apparently:
def test3(self):
self.someDialog = QtWidgets.QDialog()
self.someDialog.show()
In the case above, the visual result will be probably the same as test2, but if test3 is called again while the dialog is still visible, it will delete the current one and create a new one: that's because the dialog has no parent, and overwriting self.someDialog with a new instance will cause python to release the previous one, which will tell Qt that it can safely erase it, since it has no parent.
Finally, it's not necessary to set the parent to widgets, as long as they are added to a layout (which is the preferred approach, as using fixed geometries is highly discouraged), since the layout will automatically set the parent anyway as soon as it's [going to be] set on the actual parent.
Note that QDialog is normally shown with exec_() in order to correctly display it as modal of the parent and use it as it should.
Note the import statements at the top
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton
QLabel and QLineEdit as can be seen in the documentations
https://doc.qt.io/qtforpython/PySide2/QtWidgets/QLabel.html
https://doc.qt.io/qtforpython/PySide2/QtWidgets/QLineEdit.html
both take a QWidget as a parameter. The self passed as the second parameter indicates the current QWidget type. For other methods inside the class, self is used to invoke the instance methods.
Why doesn't clicking on a child element propagate to the parent?
from tkinter import *
root = Tk()
def handler(event):
print('clicked at', event.x, event.y)
frame = Frame(root, width=100, height=100)
label = Label(frame, text="Label")
frame.bind('<Button-1>', handler)
frame.pack()
label.pack(side=TOP)
root.mainloop()
When I run that, clicking on the label doesn't fire the handler. I've understood that events propagate to parents by default and if you didn't want that, you'd have to return "break"
You are incorrect in your original understanding that events propagate to their parent. They do not.
Admittedly, there's an edge case for widgets which are a direct descendant of a toplevel or root window. Even there, it's not that they are propagating to their parent, but rather they are being handled by other bindings as defined by the bind tags, and by default every widget has it's toplevel window as one of it's bind tags.
If you want to set a binding to work everywhere you can use the bind_all method, since each widget has an "all" bindtag by default. Another option is to give several widgets the same bindtag (using the bindtags method), then bind to that bindtag with bind_class. Which choice you make depends on what you are trying to accomplish.
bindtags are extremely powerful -- arguably more powerful than any binding mechanisms from any other toolkit. For example, if you need to have events propagate you can do that by adjusting the bindtags of every widget to include all of its ancestors. In my experience, however, such shenanigans is rarely ever needed.
You're mistaken. "break" causes that event to not propagate to other handlers for the widget that was clicked on.
In other words, if you bound your action to label and then you bound another action to the first button onto label, both callbacks will be called (unless you return "break" from the first one to be called.)
I'm not sure of a workaround though ... (We might need to wait for BryanOakley to show up ;)
In the following block, clicking on a_frame triggers the event handler on_frame_click, but clicking on a_label which is a child of a_frame does not. Is there a way to force a_frame to trap and handle events which originated on it's children (preferably with out having to add handlers to the children directly)? I am using Python 3.2.3.
import tkinter
def on_frame_click(e):
print("frame clicked")
tk = tkinter.Tk()
a_frame = tkinter.Frame(tk, bg="red", padx=20, pady=20)
a_label = tkinter.Label(a_frame, text="A Label")
a_frame.pack()
a_label.pack()
tk.protocol("WM_DELETE_WINDOW", tk.destroy)
a_frame.bind("<Button>", on_frame_click)
tk.mainloop()
Yes, you can do what you want, but it requires a bit of work. It's not that it's not supported, it's just that it's actually quite rare to need something like this so it's not the default behavior.
TL;DR - research "tkinter bind tags"
The Tkinter event model includes the notion of "bind tags". This is a list of tags associated with each widget. When an event is received on a widget, each bind tag is checked to see if it has a binding for the event. If so, the handler is called. If not, it continues on. If a handler returns "break", the chain is broken and no more tags are considered.
By default, the bind tags for a widget are the widget itself, the widget class, the tag for the toplevel window the widget is in, and finally the special tag "all". However, you can put any tags you want in there, and you can change the order.
The practical upshot of all this? You can add your own unique tag to every widget, then add a single binding to that tag that will be processed by all widgets. Here's an example, using your code as a starting point (I added a button widget, to show this isn't something special just for frames and labels):
import Tkinter as tkinter
def on_frame_click(e):
print("frame clicked")
def retag(tag, *args):
'''Add the given tag as the first bindtag for every widget passed in'''
for widget in args:
widget.bindtags((tag,) + widget.bindtags())
tk = tkinter.Tk()
a_frame = tkinter.Frame(tk, bg="red", padx=20, pady=20)
a_label = tkinter.Label(a_frame, text="A Label")
a_button = tkinter.Button(a_frame, text="click me!")
a_frame.pack()
a_label.pack()
a_button.pack()
tk.protocol("WM_DELETE_WINDOW", tk.destroy)
retag("special", a_frame, a_label, a_button)
tk.bind_class("special", "<Button>", on_frame_click)
tk.mainloop()
For more on bindtags, you might be interested in my answer to the question How to bind self events in Tkinter Text widget after it will binded by Text widget?. The answer addresses a different question than the one here, but it shows another example of using bind tags to solve real world problems.
I can't seem to find a direct method of automatically binding to child widgets (though there are methods of binding to an entire class of widgets and to all widgets in an application), but something like this would be easy enough.
def bind_tree(widget, event, callback, add=''):
"Binds an event to a widget and all its descendants."
widget.bind(event, callback, add)
for child in widget.children.values():
bind_tree(child, event, callback, replace_callback)
Just thought of this, but you could also put a transparent widget the size of a_frame on top of everything as a child of a_frame and bind the <Button> event to that, and then you could refer to a_frame as e.widget.master in the callback in order to make it reusable if necessary. That'd likely do what you want.
Based on what it says in the Levels of Binding section of this online Tkinter reference, it sounds like it's possible because you can bind a handler to three different levels.
To summarize:
Instance Level: Bind an event to a specific widget.
Class Level: Bind an event to all widgets of a specific class.
Application Level: Widget independent -- certain events always invoke a specific handler.
For the details please refer to the first link.
Hope this helps.
Depending on what you're trying to do, you could bind everything
print(a_label.bindtags()) # ('.!frame.!label', 'Label', '.', 'all')
tk.bind_class('.', "<Button>", on_frame_click)
Im making a simplish widget that can act as a container for other widgets. One of the features of the widget is that you can expand/collapse it by clicking on it. My current method is basically looking up all child widgets of the layout and hiding them. I'm looking for any help on how to handle this properly - my current implementation has at least one serious caveat: that you can't add widgets while it's collapsed (they're added in an 'unhidden' state)
heres the setCollapsed method that is run when the widget is clicked
def collapsed(self):
return self._isCollapsed
def setCollapsed(self, collapseBool):
self._isCollapsed = collapseBool
if self.layout()!=None:
childWidgets = [self.layout().itemAt(i).widget() for i in range(self.layout().count())]
for w in childWidgets:
if isinstance(w,QtGui.QWidget):
w.setHidden(collapseBool)
if collapseBool:
self._cachedMargin = self.layout().margin()
self.layout().setMargin(0)
else:
self.layout().setMargin(self._cachedMargin)
Rather than hiding all child widgets individually, I would just hide a single parent item.