Can You Implement OSX-Native Tab+Space Navigation with ttk.Button()? - python-3.x

Summarize the Problem
I am looking to use tkinter to implement the actions of tabbing and using the spacebar to cycle and activate UI elements, respectively. I am hoping to do so in a way that mimics the native behavior of OSX, as seen in the attachment below. This works fine on most other widgets in ttk.
This is made up of the following:
Allow components to be "focused" by the user using the tab key
Cause components to become visually highlighted when focused
Component can be triggered using the spacebar while focused
And the tricky part, it uses (close to) the native GUI appearance of the Operating System
Other StackOverflow Questions About This
The closest answer on this site I could find was this question. It may be important to note that the question is asked about Python 2.7.9. This question is interesting, because it suggests that changing a tk.Button() to ttk.Button() alleviates the issue, while when testing the code, I have found the opposite to be true. A tk.Button() will (inelegantly) highlight on OSX, whereas a ttk.Button() provides no visual feedback. Below is the code (with imports modified to run in 2021 on Python 3.X)
import tkinter as tk
from tkinter import ttk
class App(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.master = master
entry1 = tk.Entry(self)
entry1.pack()
entry2 = tk.Entry(self)
entry2.pack()
# CHANGE BELOW LINE TO A ttk.Button() TO TEST
button1 = tk.Button(self, text="Test", command=self.yup)
button1.pack()
def yup(self):
print("yup")
root = tk.Tk()
app = App(root).pack()
root.mainloop()
My Attempted Solutions
Solution One: Implement buttons normally using ttk.Button() with ttk.Style() to indicate focus
Pros:
Uses native OS style
Accepts focus via tab and activation via spacebar
Cons:
Does not visually indicate focus unless forced to with ttk.Style()
To the extent of my knowledge cannot be given a highlighted border by ttk.Style(), so we must settle for colored text
Example Code:
from tkinter import Tk
from tkinter import ttk
root = Tk()
def one():
print("one")
def two():
print("two")
style = ttk.Style()
style.configure('C.TButton')
style.map('C.TButton',
foreground = [('pressed','red'),('focus','blue')],
background = [('pressed','!disabled','red'),('focus','white')],
relief=[('pressed', 'sunken'),
('!pressed', 'raised')])
# Define lower GUI
B1 = ttk.Button(root, text='Button One', command=one, style='C.TButton')
B1.pack(padx=20, pady=(20,0))
B2 = ttk.Button(root, text='Button Two', command=two, style='C.TButton')
B2.pack(padx=20, pady=10)
root.mainloop()
Solution Two: Use tk.Button() instead
Pros:
Accepts focus via tab and activation via spacebar
Natively highlights button using a border
Cons:
Does not look that appealing, border is misaligned and a plain rectangle
I cannot get many parameters to work properly on OSX, particularly activebackground and highlightthickness, limiting aesthetic options.
Example Code:
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
def one():
print("one")
def two():
print("two")
B1 = tk.Button(root, text='Button One', command=one)
B1.pack(padx=20, pady=(20,0))
B2 = tk.Button(root, text='Button Two', command=two)
B2.pack(padx=20, pady=10)
root.mainloop()
Solution Three: Use the tkmacosx library's Button()
Pros:
Custom made for this problem
Highlights on tab-press with OSX-like style
Really, just everything I'm looking for
Cons:
Does not trigger button on spacebar
This last part is interesting, because based on the documentation (takefocus), this should be the expected behavior. On my machine, this is not the case (Catalina 10.15.7)
Example Code:
from tkinter import Tk
from tkmacosx import Button
root = Tk()
def one():
print("one")
def two():
print("two")
B1 = Button(root, text='Button One', command=one)
B1.pack(padx=20, pady=(20,0))
B2 = Button(root, text='Button Two', command=two)
B2.pack(padx=20, pady=10)
root.mainloop()
Concluding
Historically I understand that tkinter and OSX have not always played together perfectly, and that if I want more precise native control, I might switch to QT. I am immensely thankful for the existence of tkinter, and hope I am not asking too much.
I do want to be sure however that I'm not making an error before I try forking a repo or pursuing other solutions.
Regarding tkmacosx, it seems like this solution should be working the way it is described in the documentation, and I was hoping to get confirmation of this problem from another user, to see if it would be appropriate to file an issue on the github page.
Thank you very much for reading this post. Please feel free to ask for any additional info!

Related

Restoring deleted picture on canvas in python tkinter

I am creating a program where I make use of a check button to delete and recover a background image in python tkinter. I got the deleting part. Can someone help with the recovering part? I want to recover the image on clicking the check button.
You can do the same with your background image as I have done with the hi variable.
Like this:
import tkinter as tk
root = tk.Tk()
var = tk.IntVar()
def test():
if var.get():
hi.grid_remove()
else:
hi.grid(column=0, row=1)
hi = tk.Label(text="hi")
hi.grid(column=0, row=1)
c_b = tk.Checkbutton(text="Check", variable=var, command=test)
c_b.grid(column=0, row=0)
root.mainloop()
Note: Don't use destroy(). Use grid.remove or grid.forget() instead, otherwise your image won't be recovered.
I prefer using grid.remove, because if you need to do some change to a widget after making it disappear. Then to get it back grid.forget also won't work properly.

Tkinter textvariable does not work in a secondary window?

Because when I use textvariable from a secondary window called from a command in another window the variable.set () is not reflected in that secondary window.
example:
import tkinter as tk
def test():
ven=tk.Tk()
v1=tk.StringVar()
v1.set('TEST')
print(v1.get())
tk.Label(ven, textvariable=v1).pack()
ven.mainloop()
win=tk.Tk()
tk.Button(text='BOTON',command=test).pack()
win.mainloop()
In this case the message 'TEST' set through 'set' is not registered in the Label textvariable..
Why does this happen?
Your problem comes from the fact that you have several Tk instances running simultaneously. Tkinter is based on the the Tk gui framework which is a tcl library. Therefore each Tk instance is not just a window, it's also a tcl interpreter, therefore, when you have several Tk instances, they cannot share StrinVar because the value of the StrinVar is defined in one interpreter (here win) which does not communicate with the other one (ven).
To avoid this kind of issue, just don't use several Tk instances, use Toplevel windows instead:
import tkinter as tk
def test():
ven = tk.Toplevel(win)
v1 = tk.StringVar(win)
v1.set('TEST')
print(v1.get())
tk.Label(ven, textvariable=v1).pack()
win = tk.Tk()
tk.Button(text='BOTON', command=test).pack()
win.mainloop()

Stuck in infinite loop after destroying parent window (python, Tkinter)

The problem I am encountering is that I appear to be stuck in an infinite loop, (If I am not, please correct me). I am using tkinter for python 3.6 (64 bit) on windows 10.
In the module I am having an issue with I have 3 entry widgets and 2 buttons. Both buttons call the "destroy()" function in order to kill the parent window.
Below is a heavily abstracted version of my module, the purpose of the module is to take inputs from the entry widget and write them to a file.
def Create():
parent = Tk()
parent.MakeItlookNice
entry1 = Entry(parent)
entry1.insert(INSERT, "Please enter your desired username here")
entry2 = Entry(parent)
entry2.insert(INSERT, "Please enter your desired password here")
entry3 = Entry(parent)
entry3.insert(INSERT, "What is your mother's maiden name")
Submit = tk.Button(parent,
text ="Click here to submit your stuff",
command = lambda: [parent.destroy(),
submit.function()])
Cancel = tk.Button(parent,
text ="Click here to cancel your request",
command = lambda: parent.destroy())
parent.mainloop()
This function is contained within the module "RegisterNewUser". The "Menu" module is the module that called this function. As far as I am aware once parent.destroy() is called there is no more code to execute since it is all contained within parent.mainloop(), therefore the function is finished and the "Menu" module should continue executing.
What should happen:
I want the Submit button to destroy the window, execute the function and then return to the "Menu" module.
I want the cancel button to destroy the window and return to the "Menu" module.
What actually happens:
The window closes, like it is supposed to
But the code inside the "Menu" module does not start executing again
When I go to close the python shell, it warns me that the program is still running
Ultimately my question is, what code is still running and why hasn't it stopped?
Thank you for reading this and if you require more detail please let me know.
EDIT: I have done research on this topic before posting this question. I have read the documentation on both the tk.destroy() function and the tk.mainloop() function, I have also opened up the Tkinter module in IDLE to try and understand what happens at a deeper level but after all this, I was still unable to figure out a solution. This is my first question on stack overflow, please forgive me if I have done anything wrong.
Hmmm, so you say multiple windows? an easier way to achieve a complex UI as such is using a concept called frames. Tkinter allows you to completely change you screen and layout if you switch to a new frames. This might require you to reprogram alot of code. for an example see Switch between two frames in tkinter
Also, Some guy built a really nice Bitcoin monitoring app using tkinter and frames on youtube
I think you would probably benefit from using Toplevel() here.
I have taken the code you provided and added it to a class used to create the main window and to manage the pop up window.
I noticed a few things with you code.
Its obvoious you are importing tkinter twice like this:
import tkinter as tk
from tkinter import *
I can tell from how you have written your entry fields vs your buttons. Do not do this. Instead just used one or the other. I recommend just using import tkinter as tk.
You are using a function to create a new tkinter instance and judging by your question you all ready have a tkinter instance created for your menu. Instead of creating new Tk() instances you can use Toplevel() instead to open a new window that can inherit everything from the main window and should be easier to manage.
You do not really need to use lambda in this situation. I have also removed the lambda function and replaced with a simple command that will work here.
Take a look at the below code and let me know if you have any questions.
import tkinter as tk
class MyApp(tk.Frame):
def __init__(self, master, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
self.master = master
self.master.title("Main MENU")
tk.Button(self.master, text="Open top level", command = self.create).pack()
def create(self):
self.top = tk.Toplevel(self.master)
entry1 = tk.Entry(self.top, width = 35)
entry1.pack()
entry1.insert(0, "Please enter your desired username here")
entry2 = tk.Entry(self.top, width = 35)
entry2.pack()
entry2.insert(0, "Please enter your desired password here")
entry3 = tk.Entry(self.top, width = 35)
entry3.pack()
entry3.insert(0, "What is your mother's maiden name")
tk.Button(self.top, text ="Click here to submit your stuff",
command = self.Submit).pack()
tk.Button(self.top, text ="Click here to cancel your request",
command = self.top.destroy).pack()
def Submit(self):
print("Do something here to submit data")
self.top.destroy()
if __name__ == "__main__":
root = tk.Tk()
app1 = MyApp(root)
tk.mainloop()
You can use toplevel() and its library function wait_window() just prior to (or instead of) your mainloop() and your problem will be solved
wait_window() mentioned above worked for me in the code below replacing popup.mainloop() with it, the mainloop() kept my code in an infinite loop upon the popup even after okay button was hit

Adding items to a Listbox using Entries from another class

I have created two classes, one is in People.py (which will be the parent class) which contains a list box that can be populated by just opening a file and adding content to the list box line by line.
Another class is in Names.py (which I want it to be child class), which contains entries of first name, last name, and a combo box for titles that should (will implement once question/problem is answered) go into the list in the main window where the class is People. I am trying to use an OOP model. Right now, it's not fully OOP, but I will refactor the code later.
I have tried posting this code before but people are having trouble running it due to indentation problems, so I'm providing the links to the classes. In Dropbox Name Class and People Class:
Note: I'm running this in a Linux environment, so you may have to modify the file choosing line in the People class if using Windows (or another OS).
f = os.path.expanduser('~/Desktop')
Actually, you still have a problem of inconsistent use of tabs and spaces, which I solved, but maybe other people cannot solve it.
First of all, you should name your files/modules with lower cases (by convention, and you should follow it!). Then, in Names.py you are doing from Tkinter import * and then from Tkinter import Tk, which does not make any sense: with the first you are already importing Tk.
Your problem is the following:
People.people_list.insert(Tk.END, FirstName.get()) AttributeError:
'module' object has no attribute 'people_list'
In fact, you are trying to access an inexistent attribute of the module People called people_list, which is a local variable to some functions, from what I have been seeing.
If you want to fill a Listbox which is a property of some Toplevel A, with the input from another Toplevel B, you should pass a reference of the Toplevel A to B, maybe during its construction.
Here you have an example:
from tkinter import * # python 3
class Other(Toplevel):
"""Note that the constructor of this class
receives a parent called 'master' and a reference to another Toplevel
called 'other'"""
def __init__(self, master, other):
Toplevel.__init__(self, master)
self.other = other # other Toplevel
self.lab = Label(self, text="Insert your name: ")
self.lab.grid(row=0, column=0)
self.entry = Entry(self)
self.entry.grid(row=0, column=1)
# When you click the button you call 'self.add'
self.adder = Button(self, text='Add to Listbox', command=self.add)
self.adder.grid(row=1, column=1)
def add(self):
"""Using a reference to the other window 'other',
I can access its attribute 'listbox' and insert a new item!"""
self.other.listbox.insert("end", self.entry.get())
class Main(Toplevel):
"""Window with a Listbox"""
def __init__(self, master):
Toplevel.__init__(self, master)
self.people = Label(self, text='People')
self.people.grid(row=0)
self.listbox = Listbox(self)
self.listbox.grid(row=1)
if __name__ == "__main__":
root = Tk()
root.withdraw() # hides Tk window
main = Main(root)
Other(root, main) # passing a reference of 'main'
root.mainloop()
I noticed also that you are using 2 instance of Tk for each of your windows, which is bad. You should use just one instance of Tk for every application. If you want to use multiple windows, just use Toplevels, as I mentioned.
In general, your structure is not so good. You should start by creating simple good applications, and then pass to big ones once you grasp the basic concepts.

How do I set the minimum size of a Gtk ButtonBox child?

I'm trying to set the minimum size of the buttons in this GtkButtonBox. Currently they seem to be fixed - approx 85 pixels I think.
Is this possible?
If not, is there another way in Gtk to get two small sized buttons to snuggle together like in the above picture rather than having them appear to be two separate buttons? For example GtkStackSwitcher may be something I could use but there doesn't appear to be a way to respond to click events for a button.
I've used this test program to create the above (Ubuntu 14.04, Gtk+3.10 and Python3):
from gi.repository import Gtk
import sys
class MyWindow(Gtk.ApplicationWindow):
def __init__(self, app):
Gtk.Window.__init__(self, title="example", application=app)
self.set_default_size(350, 200)
self.set_border_width(10)
hbox = Gtk.ButtonBox.new(Gtk.Orientation.HORIZONTAL)
hbox.set_layout(Gtk.ButtonBoxStyle.EXPAND)
button = Gtk.Button(label="a")
hbox.add(button)
button2 = Gtk.Button(label="b")
hbox.add(button2)
self.add(hbox)
class MyApplication(Gtk.Application):
def __init__(self):
Gtk.Application.__init__(self)
def do_activate(self):
win = MyWindow(self)
win.show_all()
def do_startup(self):
Gtk.Application.do_startup(self)
app = MyApplication()
exit_status = app.run(sys.argv)
sys.exit(exit_status)
With regards to a question about the desktop environment I'm using.
I've tried Mate, Unity and Gnome-Shell. All work the same way. I've removed the title and those controls. Still the same thing happens. To me this looks more like a GTK issue.
I believe that GtkButtonBox imposes some layout constraints on its buttons that you may not want here. Try using buttons in just a regular GtkGrid, but give them the GTK_STYLE_CLASS_LINKED CSS class.
For each button, do:
button.get_style_context().add_class(Gtk.STYLE_CLASS_LINKED)

Resources