Tkinter maximize/restore/resize differentiation - python

I know in Tkinter that the "<Configure>" event handles size changes in a window. However, I need to distinguish between when a user hits the maximize button, the restore button and when the user is resizing the window (instead of all three at once). Any ideas on how to do this? Is there a standard way? For instance, when a user hits maximize, I want to execute my code to maximize. When the user hits restore, I want to execute different code to restore. When the user drags to resize (or uses the keyboard shortcut to do so) I want it to execute different code altogether.

I can't think of a built-in way to track these events, but you could use the state() method on your root window to track changes. You can check the returned values of state(), specifically normal and zoomed (looks like Windows and OSX only), and call your own methods to handle the resize type based off those values. Here's a example to clarify:
class App(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
# initialize the new_state
self.new_state = 'normal'
self.parent.bind('<Configure>', self._resize_handler)
def _resize_handler(self, event):
self.old_state = self.new_state # assign the old state value
self.new_state = self.parent.state() # get the new state value
if self.new_state == 'zoomed':
print('maximize event')
elif self.new_state == 'normal' and self.old_state == 'zoomed':
print('restore event')
else:
print('dragged resize event')
root = Tk()
App(root).pack()
root.mainloop()
If you want to distinguish between dragging the window and dragging to resize, you'll have to add some extra checks, maybe storing the size before <Configure> and the size after, with winfo_height() and winfo_width(), and if no change occurs, you know the window was only repositioned.
Hope that helps.

Related

setChecked radiobutton of another group pyqt

I have 2 radiobuttons created (inside a QMainWindow class) like:
def dtype_radiobuttons(self):
layout = QHBoxLayout()
rb1 = QRadioButton("complex")
rb1.toggled.connect(lambda: self.update_image("dtype", rb1.text()))
self.real_dtype_rb = QRadioButton("real", self)
self.real_dtype_rb.toggled.connect(lambda: self.update_image("dtype", self.real_dtype_rb.text()))
self.btngroup.append(QButtonGroup())
self.btngroup[-1].addButton(self.real_dtype_rb)
self.btngroup[-1].addButton(rb1)
rb1.setChecked(True)
layout.addWidget(rb1)
layout.addWidget(self.real_dtype_rb)
layout.addStretch()
return layout
def library_radiobutton(self):
layout = QHBoxLayout()
self.cvnn_library_rb = QRadioButton("cvnn", self)
self.cvnn_library_rb.toggled.connect(lambda: self.update_image("library", self.cvnn_library_rb.text()))
rb2 = QRadioButton("tensorflow", self)
rb2.toggled.connect(lambda: self.update_image("library", rb2.text()))
self.btngroup.append(QButtonGroup())
self.btngroup[-1].addButton(rb2)
self.btngroup[-1].addButton(self.cvnn_library_rb)
self.cvnn_library_rb.setChecked(True)
layout.addWidget(self.cvnn_library_rb)
layout.addWidget(rb2)
layout.addStretch()
return layout
I want to make it impossible to select the complex option of the dtype radiobuttons group and tensorflow radiobutton of the library radiobuttons. Leaving 3 out of the 4 possible combinations. So if I select complex and library was tensorflow, I want to automatically change the library to cvnn. I tried to implement it like this:
def update_image(self, key, value):
if value == "complex":
if hasattr(self, 'cvnn_library_rb'): # It wont exists if I still didnt create the radiobutton.
self.cvnn_library_rb.setChecked(True) # Set library cvnn
elif value == "tensorflow":
if hasattr(self, 'real_dtype_rb'):
self.real_dtype_rb.setChecked(True) # Set real dtype
... Do the other stuff I need to do.
The weird thing is that it actually works in the sense that, for example, if I am on complex activated and select tensorflow, the radiobutton changes to real (what I want!) but tensorflow does not get selected! I need to select it again as if making self.real_dtype_rb.setChecked(True) cancels the selection of the radiobutton I clicked on. (Very weird if you ask me).
The hasattr is used because depending on the order I call the
functions, there are some radiobuttons that will be created before
the other, so it might not exist.
This
is an option I am considering but it's disabling the radiobutton
group instead of changing their state (not what I prefer).
The signal toggled is triggered whenever you change the state of your radio buttons. So, it will be triggered when you call setChecked (once for the radio button you toggle and once for the other you untoggle) and update_image is called is the wrong case.
You have to check the state of the radio button and call update_image only if the radio button is toggled:
rb2.toggled.connect(lambda state: state and self.update_image("library", rb2.text(), state))

How do I bind callbacks to a listbox using tkinter?

I'm playing around with tkinter and am struggling somewhat with the use of listboxes. I want to bind an arbitrary function to a selection event. As I understand it - it should go something like this.
import Tkinter as tk
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
lb = tk.Listbox(self)
lb.insert("end", "one")
lb.insert("end", "two")
lb.insert("end", "three")
lb.bind("<Double-Button-1>", self.OnDouble)
lb.pack(side="top", fill="both", expand=True)
def OnDouble(self, event):
widget = event.widget
selection=widget.curselection()
value = widget.get(selection[0])
print "selection:", selection, ": '%s'" % value
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
My understanding is that when an element in the list box is double-clicked, a virtual event is created which can be bound to a callback function - in this case the OnDouble function defined.
I'm a little confused about the intricacies of this, would someone be able to explain exactly what is happening in this OnDouble function?
I'm trying to trigger an event when an element is selected from a listbox - i'm using pygubu to design the GUI but I understand that the syntax still follows tkinter conventions:
class TestApp:
def __init__(self):
self.builder = builder = pygubu.Builder()
builder.add_resource_path(PROJECT_PATH)
builder.add_from_file(PROJECT_UI)
self.mainwindow = builder.get_object('toplevel_1')
builder.connect_callbacks(self)
self.box = builder.get_object('listbox_1')
self.list = [1,2,3,4]
self.box.insert(tk.END, self.list[0])
self.box.insert(tk.END, self.list[1])
self.box.insert(tk.END, self.list[2])
self.box.insert(tk.END, self.list[3])
self.console = builder.get_object('text_1')
def print_console(self, msg):
self.console.configure(state="normal")
self.console.insert(tk.END, msg+"\n")
self.console.configure(state="disabled")
self.console.update_idletasks()
def run(self):
self.mainwindow.mainloop()
if __name__ == '__main__':
app = TestApp()
app.run()
Essentially I want the console to print (using the defined command) something like f'You have selected number {number}' when said number is double clicked on the list box. How can I do this? I would appreciate the workings behind it, so I can apply it to more complex exercises, and understand the solution rather than just copy/paste it.
Items in a listbox can be selected with a single click, but they can also be selected via the keyboard. For this reason, binding to a specific physical event such as <Double-1> is not the best way to be notified when the selection changes.
In the specific case of the listbox, binding to <<ListboxSelect>> is the best event to bind to, as that event will be generated after the selection has been set regardless of how it was set.
For example:
lb.bind("<<ListboxSelect>>", self.onSelect)
When a function is called via a binding it will automatically be passed an argument that contains details about the event that triggered the function call. One of the parameters is widget, which is a reference to the widget that received the call.
In your specific case you could ignore that and use self.listbox within the bound function, but using event.widget is a good habit to get into. That is why the code you copied starts with widget = event.widget
The next step is to get the selected item or items. In your case only a single selection is allowed at a time. However, the curselection method always returns a list. selection[0] will refer to the item selected, if there is a selection.
Note: the <<ListboxSelect>> event is triggered both when the selection is set or it is unset, so the code in your question could fail if the items are deselected by some means.
Finally, once you have the index of the selected item (eg: selection[0]), you can use that numerical index to get the text of the selected item. That is what the line value = widget.get(selection[0]) is doing.

How to sound a bell when the user clicks outside a modal window?

The situation is simple. I have a main window with a Help - About menu.
When this menu item is clicked, a modal window is opened (let's say it's an About-window).
With self.grab_set() I disabled the main-window (although the modal window does flicker when you click the main title bar).
So far, so good.
Here is the question: I really like to sound a bell when the user clicks outside the modal window on the main window.
This is what I could find about grab_set(), really not that much:
[effbot] ...a method called grab_set, which makes sure that no mouse or keyboard
events are sent to the wrong window.
[effbot] Routes all events for this application to this widget.
[kite.com] A grab directs all events to this and descendant widgets in the application.
[google books] grab_set() ensures that all of the application's events are sent to w until a corresponding call is made to grab_release ([Me:] or till the window is destroyed?)
I'm not quite sure how to understand this: does it mean you can handle an event on the main window within the modal window (like sounding my bell)?
So I tried things like:
self.bind('<Button-1>', self.bell) Exception in Tkinter callback: _tkinter.TclError: bad window path name
parent.bind('<Button-1>', self.bell) Nothing happens
So, how to sound a bell like when clicked outside the modal window on the main window, like in so many other applications?
Derived questions:
Is it still possible to cature events from the main window after using
grab_set for the modal window?
Is there a way to prevent the flickering?
I really like to understand this mysterious grab_set() method.
Stripped code:
import tkinter as tk
class About(tk.Toplevel):
def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.geometry('200x150')
#--- OK button
btn_ok = tk.Button(self, text='OK', command=self.destroy) # destroy with OK
btn_ok.pack(side=tk.TOP)
btn_ok.focus() # destroy with spacebar
#--- Make window modal
self.grab_set()
# self.wait_window() # is this necessary in this case?
# self.bind('<Button-1>', self.bell) ??? The question
class MenuBar(tk.Menu):
def __init__(self, parent):
tk.Menu.__init__(self)
helpmenu = tk.Menu(self, tearoff=0)
helpmenu.add_command(label='About', command=lambda: About(parent))
self.add_cascade(label='Help', menu=helpmenu)
class MainApp():
def __init__(self, parent):
parent.configure(background='#000000')
parent.geometry('800x600')
menubar = MenuBar(parent)
parent.configure(menu=menubar)
if __name__ == '__main__':
root = tk.Tk()
MainApp(root)
root.mainloop()
When you set a grab, all button clicks will go to the window with the grab. You capture them the way you capture any other event. In the case of a button click you do that by binding a function to <1>.
It's important to know that a binding on a root window or a Toplevel window will apply to all widgets in that window. For example, binding to self in your code will fire even when you click on the "Ok" button. Therefore, the callback should probably only do work when the widget associated with the event is the same as the toplevel.
Example:
class About(tk.Toplevel):
def __init__(self, parent):
...
self.bind("<1>", self.capture_click)
...
def capture_click(self, event):
if event.widget == self:
<your logic here>
In the case of wanting to know if the user clicked outside the window, you can use the coordinates of the event object to compare against the window to see if the click is inside or outside.
def on_click(self, event):
if event.widget == self:
if (event.x < 0 or event.x > self.winfo_width() or
event.y < 0 or event.y > self.winfo_height()):
self.bell()
I found a second solution. Though my question was explicitly about using grab_set(), this method does the same for me: making the window as modal as possible and sound a bell.
Instead of using self.grab(), you can also disable the parent window:
parent.attributes('-disabled', True)
Of course it needs to be enabled again when the OK button is clicked (and when the windows is closed with the [X] close control. However, my original About-window has no window decoration). The command for the OK-button becomes:
btn_ok = tk.Button(self, text='OK', command=lambda: self.closeme(parent))
...which calls the closeme function:
def closeme(self, parent):
parent.attributes('-disabled', False)
self.destroy()
The bell sounds automatically when clicking a disabled window.
Method 1: Keeps you in full control of the main window but does not 'freeze' the main window: you can still move it around.
Method 2: Completely freezes the main window, but if it happens to be (partially) covered by another window (not of this application), then you can only bring back to top using Alt+Tab (windows).
I'm sure I will use both techniques in the future depending on my needs.

Binding callbacks to minimize and maximize events in Toplevel windows

I've read through related answers and it seems that the accepted way to do this is by binding callbacks to <Map> and <Unmap> events in the Toplevel widget. I've tried the following but to no effect:
from Tkinter import *
tk = Tk()
def visible(event):
print 'visible'
def invisible(event):
print 'invisible'
tk.bind('<Map>', visible)
tk.bind('<Unmap>', invisible)
tk.mainloop()
I'm running python 2.7 on Linux. Could this be related to window manager code in different operating systems?
Calling tk.iconify() before tk.mainloop() has no effect either. In fact, the only command that produces the correct behavior is tk.withdraw() which is certainly not the same thing as minimizing the window. Additionally, if <Map> and <Unmap> events are triggered by calling pack(), grid(), or place(), why is <Map> triggered when the application window is minimized on Windows and/or Mac, as this and this answer suggest. And why would they be triggered when calling withdraw() and deiconify() on Linux?
Unmapping on Linux
The term Unmap has a quite different meaning on Linux than it has on Windows. On Linux, Unmapping a window means making it (nearly) untraceable; It does not appear in the application's icon, nor is it listed anymore in the output of wmctrl -l. We can unmap / map a window by the commands:
xdotool windowunmap <window_id>
and:
xdotool windowmap <window_id>
To see if we can even possibly make tkinter detect the window's state minimized, I added a thread to your basic window, printing the window's state once per second, using:
root.state()
Minimized or not, the thread always printed:
normal
Workaround
Luckily, if you must be able to detect the window's minimized state, on Linux we have alternative tools like xprop and wmctrl. Although as dirty as it gets, it is very well scriptable reliably inside your application.
As requested in a comment, below a simplified example to create your own version of the bindings with external tools.
How it works
When the window appears (the application starts), We use wmctrl -lp to get the window's id by checking both name and pid (tkinter windows have pid 0).
Once we have the window id, we can check if the string _NET_WM_STATE_HIDDEN is in output of xprop -id <window_id>. If so, the window is minimized.
Then we can easily use tkinter's after() method to include a periodic check. In the example below, the comments should speak for themselves.
What we need
We need both wmctrl and xprop to be installed. On Dedian based systems:
sudo apt-get install wmctrl xprop
The code example
import subprocess
import time
from Tkinter import *
class TestWindow:
def __init__(self, master):
self.master = master
self.wintitle = "Testwindow"
self.checked = False
self.state = None
button = Button(self.master, text = "Press me")
button.pack()
self.master.after(0, self.get_state)
self.master.title(self.wintitle)
def get_window(self):
"""
get the window by title and pid (tkinter windows have pid 0)
"""
return [w.split() for w in subprocess.check_output(
["wmctrl", "-lp"]
).decode("utf-8").splitlines() if self.wintitle in w][-1][0]
def get_state(self):
"""
get the window state by checking if _NET_WM_STATE_HIDDEN is in the
output of xprop -id <window_id>
"""
try:
"""
checked = False is to prevent repeatedly fetching the window id
(saving fuel in the loop). after window is determined, it passes further checks.
"""
self.match = self.get_window() if self.checked == False else self.match
self.checked = True
except IndexError:
pass
else:
win_data = subprocess.check_output(["xprop", "-id", self.match]).decode("utf-8")
if "_NET_WM_STATE_HIDDEN" in win_data:
newstate = "minimized"
else:
newstate = "normal"
# only take action if state changes
if newstate != self.state:
print newstate
self.state = newstate
# check once per half a second
self.master.after(500, self.get_state)
def main():
root = Tk()
app = TestWindow(root)
root.mainloop()
if __name__ == '__main__':
main()
My own implementation of the hack suggested by Jacob.
from Tkinter import Tk, Toplevel
import subprocess
class CustomWindow(Toplevel):
class State(object):
NORMAL = 'normal'
MINIMIZED = 'minimized'
def __init__(self, parent, **kwargs):
Toplevel.__init__(self, parent, **kwargs)
self._state = CustomWindow.State.NORMAL
self.protocol('WM_DELETE_WINDOW', self.quit)
self.after(50, self._poll_window_state)
def _poll_window_state(self):
id = self.winfo_id() + 1
winfo = subprocess.check_output(
['xprop', '-id', str(id)]).decode('utf-8')
if '_NET_WM_STATE_HIDDEN' in winfo:
state = CustomWindow.State.MINIMIZED
else:
state = CustomWindow.State.NORMAL
if state != self._state:
sequence = {
CustomWindow.State.NORMAL: '<<Restore>>',
CustomWindow.State.MINIMIZED: '<<Minimize>>'
}[state]
self.event_generate(sequence)
self._state = state
self.after(50, self._poll_window_state)
if __name__ == '__main__':
root = Tk()
root.withdraw()
window = CustomWindow(root)
def on_restore(event):
print 'restore'
def on_minimize(event):
print 'minimize'
window.bind('<<Restore>>', on_restore)
window.bind('<<Minimize>>', on_minimize)
root.mainloop()
For me, on Win10, your code works perfectly, with the caveat that the middle frame button produces 'visible' whether it means 'maximize' or 'restore'. So maximize followed by restore results in 2 new 'visibles' becoming visible.
I did not particularly expect this because this reference says Map is produced when
A widget is being mapped, that is, made visible in the application. This will happen, for example, when you call the widget's .grid() method.
Toplevels are not managed by geometry. The more authoritative tk doc says
Map, Unmap
The Map and Unmap events are generated whenever the mapping state of a window changes.
Windows are created in the unmapped state. Top-level windows become mapped when they transition to the normal state, and are unmapped in the withdrawn and iconic states.
Try adding tk.iconify() before the mainloop call. This should be doing the same as the minimize button. If it does not result in 'invisible', then there appears to be a tcl/tk bug on Linux.

Create a python tkinter window with no X (close) button

I'm writing a 'wizard' type Python Tkinter GUI that collects information from the user and then performs several actions based on the user's entries: file copying, DB updates, etc. The processing normally takes 30-60 seconds and during that time, I want to:
Provide the user with text updates on the activity and progress
Prevent the user from closing the app until it's finished what it's doing
I started on the route of having the text updates appear in a child window that's configured to be trainsient and using wait_window to pause the main loop until the activities are done. This worked fine for other custom dialog boxes I created which have OK/cancel buttons that call the window's destroy method. The basic approach is:
def myCustomDialog(parent,*args):
winCDLG = _cdlgWin(parent,*args)
winCDLG.showWin()
winCDLG.dlgWin.focus_set()
winCDLG.dlgWin.grab_set()
winCDLG.dlgWin.transient(parent)
winCDLG.dlgWin.wait_window(winCDLG.dlgWin)
return winCDLG.userResponse
class _cdlgWin():
def __init__(self,parent,*args):
self.parent = parent
self.dlgWin = tk.Toplevel()
self.userResponse = ''
def showWin(self):
#Tkinter widgets and geometry defined here
def _btnOKClick(self):
#self.userResponse assigned from user entry/entries on dialog
self.dlgWin.destroy()
def _btnCancelClick(self):
self.dlgWin.destroy()
However this approach isn't working for the new monitor-and-update dialog I want to create.
First, because there's no user-initiated action to trigger the copy/update activities and then the destroy, I have to put them either in showWin, or in another method. I've tried both ways but I'm stuck between a race condition (the code completes the copy/update stuff but then tries to destroy the window before it's there), and never executing the copy/update stuff in the first place because it hits the wait_window before I can activate the other method.
If I could figure out a way past that, then the secondary problem (preventing the user from closing the child window before the work's done) is covered by the answers below.
So... is there any kind of bandaid I could apply to make this approach work the way I want? Or do I need to just scrap this because it can't work? (And if it's the latter, is there any way I can accomplish the original goal?)
self.dlgWin.overrideredirect(1) will remove all of the buttons (make a borderless window). Is that what you're looking for?
As far as I know, window control buttons are implemented by the window manager, so I think it is not possible to just remove one of them with Tkinter (I am not 100% sure though). The common solution for this problem is to set a callback to the protocol WM_DELETE_WINDOW and use it to control the behaviour of the window:
class _cdlgWin():
def __init__(self,parent,*args):
self.parent = parent
self.dlgWin = tk.Toplevel()
self.dlgWin.protocol('WM_DELETE_WINDOW', self.close)
self.userResponse = ''
def close(self):
tkMessageBox.showwarning('Warning!',
'The pending action has not finished yet')
# ...

Categories