I have some programs that use dialog boxes with groups of radio buttons. The expected behavior when initially designating a button with setChecked(True) is that it will be assigned the tab stop for its group. That's how it works in the Windows API. But it appears that the tab stop always goes to the first button in the group (even though it's not checked) until you manually re-select the intended button with a mouse click or arrow key. You can use setFocus(True) on a designated button, but this only can be used for one group of buttons. This happens with Windows 10 and Ubuntu, using PyQt5 versions 5.12 and 5.15. I've tried the various radio button focus-related functions listed in the Qt documentation without success.
This question was asked 2 years ago (53645767/radio-button-tab-sequencing), without an answer that explains definitively either how to set the tab stops, or that it's not an option.
I adapted this script from a Web tutorial. For both groups, I initialize the second button in the group with setChecked(True), and use setFocus(True) on the button in the first group. Cycling with "Tab" shows the tab stop at the second button of the first group as intended, but it stays with the first (unchecked) button of the second group until you click the second button or use the arrow key to re-select it. Am I missing something here or it this an intentional "feature"?
def init_ui(self):
self.label = QLabel('What is your favorite color?')
self.rbtn1 = QRadioButton('blue')
self.rbtn2 = QRadioButton('red')
self.label2 = QLabel("")
self.label3 = QLabel('What is your favorite element?')
self.rbtn3 = QRadioButton('bolognium')
self.rbtn4 = QRadioButton('unobtainium')
self.label4 = QLabel("")
self.btngroup1 = QButtonGroup()
self.btngroup2 = QButtonGroup()
self.btngroup1.addButton(self.rbtn1)
self.btngroup1.addButton(self.rbtn2)
self.btngroup2.addButton(self.rbtn3)
self.btngroup2.addButton(self.rbtn4)
self.rbtn1.toggled.connect(self.onClickedColor)
self.rbtn2.toggled.connect(self.onClickedColor)
self.rbtn3.toggled.connect(self.onClickedElement)
self.rbtn4.toggled.connect(self.onClickedElement)
layout = QVBoxLayout()
layout.addWidget(self.label)
layout.addWidget(self.rbtn1)
layout.addWidget(self.rbtn2)
layout.addWidget(self.label2)
layout.addWidget(self.label3)
layout.addWidget(self.rbtn3)
layout.addWidget(self.rbtn4)
layout.addWidget(self.label4)
self.setGeometry(200, 200, 300, 300)
self.setLayout(layout)
self.rbtn2.setChecked(True)
self.rbtn2.setFocus(True)
self.rbtn4.setChecked(True)
self.show()
def onClickedColor(self):
radioBtn = self.sender()
if radioBtn.isChecked():
self.label2.setText("Your favorite color is " + radioBtn.text())
def onClickedElement(self):
radioBtn = self.sender()
if radioBtn.isChecked():
self.label4.setText("Your favorite element is " + radioBtn.text())
That seems like a bug (btw, the question you linked only shows a similar behavior, but considering it's about HTML I believe it's unrelated).
Unfortunately, "auto-focus" management is pretty complex and is based on various aspects: order of creation, parenthood, focus policies, position of widgets and system behavior.
A possible (and "hacky") solution could be to override focusNextPrevChild().
The trick is to get the current focused widget and cycle through its own nextFocusInChain and previousInFocusChain (by checking its focus policy and recursively until a valid policy is found), and then ensure that the widget is contained in the button group.
Note that the following is a very raw implementation, and you'll probably need further tests in order to get it correctly working and ensure that no bug/recursion issue happens.
def focusNextPrevChild(self, isNext):
if isNext:
func = 'nextInFocusChain'
reason = Qt.TabFocusReason
else:
func = 'previousInFocusChain'
reason = Qt.BacktabFocusReason
current = self.focusWidget()
other = getattr(current, func)()
while True:
while not other.focusPolicy():
other = getattr(other, func)()
if isinstance(other, QRadioButton):
for group in self.btngroup1, self.btngroup2:
if other in group.buttons():
checked = group.checkedButton()
if checked == current and not isNext:
continue
if checked:
checked.setFocus(reason)
else:
other.setFocus(reason)
return True
break
return super().focusNextPrevChild(isNext)
Related
I have been searching for a way to set the tab order in a tkinter application, that I have been working on. Currently the default order seems to be working from top-down, but it requires using CTRL + Tab to cycle through the controls.
Is there any way to customize the order and, more so, change the CTRL + Tab to just Tab?
Tab order is based on the stacking order, which in turn defaults to the order that widgets are created. You can adjust the stacking order (and thus the tab order) using the methods tkraise (or lift) and lower.
This should be working out of the box for you without the need to press CTRL + Tab. Be aware, however, that tab inserts a literal tab in text widgets rather than moving focus to another control. That default behavior can be changed of course.
Here's an example showing how to reverse the tab order. When running the example, pressing tab in the first entry should take you to the last. Pressing tab again takes you to the second, then the first, lather, rinse, repeat
Note that the native Tk commands are raise and lower, but since raise is a reserved word in Python it had to be renamed in tkinter.
import Tkinter as tk
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
e1 = tk.Entry(self)
e2 = tk.Entry(self)
e3 = tk.Entry(self)
e1.insert(0,"1")
e2.insert(0,"2")
e3.insert(0,"3")
e1.pack()
e2.pack()
e3.pack()
# reverse the stacking order to show how
# it affects tab order
new_order = (e3, e2, e1)
for widget in new_order:
widget.lift()
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
Since you mention you have to do CTRL + Tab, I'm guessing you're trying to have the tab key change focus from a text widget. Normally a tab key inserts a literal tab. If you want it to change the focus, just add a binding to the <Tab> event.
Tkinter has a function that will return the name of the next widget that should get focus. Unfortunately, for older versions of Tkinter that function is buggy. However, it's easy to work around that. Here's a couple of methods you can add to the above code:
def _focusNext(self, widget):
'''Return the next widget in tab order'''
widget = self.tk.call('tk_focusNext', widget._w)
if not widget: return None
return self.nametowidget(widget.string)
def OnTextTab(self, event):
'''Move focus to next widget'''
widget = event.widget
next = self._focusNext(widget)
next.focus()
return "break"
I've been searching for ways to skip some widgets while tabbing and found in tkinter's tk_focusNext function description the following: "A widget is omitted if it has the takefocus resource set to 0."
you can set takefocus on widget initialization as an argument.
I am trying to select multiple items from a Listbox, it seems intuitive to press shift while selecting and have a block of items be selected, but there seems to be no build in functionality for this in Tkinter.
So I'm trying to implement it on my own, by registering the shift key and getting the latest selection. But I get stuck trying to figure out the latest selection in the Listbox. listbox.get(ACTIVE) seems to be one behind what I expect.
Here is what I have tried to do so far, I am aware that I need to do more when I know the newest selection but that will come later.
from Tkinter import *
class GUI():
def __init__(self,frame): # Some Init
self.listbox = Listbox(root, height=20, width=51, selectmode=MULTIPLE, exportselection=0, yscrollcommand=yscrollbar.set, xscrollcommand=xscrollbar.set)
# -- Some Grid setup here --
self.listbox.bind("<<ListboxSelect>>", self.selectionCallback)
frame.bind("<Shift_L>", self.shiftCallback)
frame.bind("<KeyRelease-Shift_L>", self.shiftCallback)
def selectionCallback(self,event):
print self.listbox.get(ACTIVE) # This is where im stuck
def shiftCallback(self,event):
if event.type is 2: #KeyPress
self.shift = True
elif event.type is 3: #KeyRelease
self.shift = False
if __name__ == "__main__":
root = Tk()
GUI(root)
The behavior you seem to want actually is available by default, use
Listbox(..., selectmode=EXTENDED, ...)
From effbot:
The listbox offers four different selection modes through the selectmode option. These are SINGLE (just a single choice), BROWSE (same, but the selection can be moved using the mouse), MULTIPLE (multiple item can be choosen, by clicking at them one at a time), or EXTENDED (multiple ranges of items can be chosen, using the Shift and Control keyboard modifiers). The default is BROWSE. Use MULTIPLE to get “checklist” behavior, and EXTENDED when the user would usually pick only one item, but sometimes would like to select one or more ranges of items.
As for listbox.get(ACTIVE), the item that is ACTIVE is the one that is underlined. You can see that this is only updated upon release of the mouse button. Because the <<ListboxSelect>> event is triggered on the mouse press, you get the previously selected item, because the ACTIVE is not updated yet.
I need to check multiple radio buttons from a qt ui with python.
Up to now we are using something similar to:
if main.ui.radioButton_1.isChecked():
responses["q1"] = "1"
elif main.ui.radioButton_2.isChecked():
responses["q1"] = "2"
elif main.ui.radioButton_3.isChecked():
responses["q1"] = "3"
if main.ui.radioButton_4.isChecked():
responses["q2"] = "1"
elif main.ui.radioButton_5.isChecked():
responses["q2"] = "2"
elif main.ui.radioButton_6.isChecked():
responses["q2"] = "3"
...
Since there are very many buttons and many different categories (q1, q2, ...) I was thinking of optimizing it a bit. So this is what I hoped would work (adopted from How to get the checked radiobutton from a groupbox in pyqt):
for i, button in enumerate(["main.ui.radioButton_" + str(1) for i in range(1, 8)]):
if button.isChecked():
responses["q1"] = str(i - 1)
I get why this doesn't work but writing it I hoped it would.
So I tried to iterate through the buttons using something similar to (Is there a way to loop through and execute all of the functions in a Python class?):
for idx, name, val in enumerate(main.ui.__dict__.iteritems()):
and then use some modulo 3 and such to assign the results. But that doesn't work either. Not sure if it's because i used __ dict __ or something else. The error I got was:
TypeError: 'QLabel' object is not iterable
Now some people could say that implicit is better that explicit and also because of readability the if elif chain is good the way it is but there are 400+ lines of that. Also after reading this post, Most efficient way of making an if-elif-elif-else statement when the else is done the most?, I thought there must be a better and more efficient way of doing this (see examples 3.py and 4.py of the of the accepted answer). Because I need to check the Boolean value of main.ui.radioButton_1.isChecked() and then assign thevalue according to the Buttons group (q1, q2,...), I haven't managed to implement the solution using dictionaries as described in the post.
Am I stuck with the if elif chain or is there a way to not only reduce the LOC but also make the code more efficient (faster)?
It looks like you have used Qt Designer to create your ui, so I would suggest putting each set of radio buttons in a QButtonGroup. This will give you a simple, ready-made API for getting the checked button in a group without having to query each button individually.
In Qt Designer, buttons can be added to a button-group by selecting them, and then choosing Assign to button group > New button group from the context menu. The button IDs (which you will need to use later) are assigned in the order the buttons are selected. So use Ctrl+Click to select each button of a group in the correct order. The IDs start at 1 for each group and just increase by one for each button that is added to that group.
When a new button-group is added, it will appear in the Object Inspector. This will allow you to select it and give it a more meaningful name.
Once you've created all the groups, you can get the checked button of a group like this:
responses["q1"] = str(main.ui.groupQ1.checkedId())
responses["q2"] = str(main.ui.groupQ2.checkedId())
# etc...
This could be simplified even further to process all the groups in a loop:
for index in range(1, 10):
key = 'q%d' % index
group = 'groupQ%d' % index
responses[key] = str(getattr(main.ui, group).checkedId())
Another way to do it is using signals. If you had lots of radio button in an application, I suspect this kind of approach would be noticeably faster. For example:
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class MoodExample(QGroupBox):
def __init__(self):
super(MoodExample, self).__init__()
# Create an array of radio buttons
moods = [QRadioButton("Happy"), QRadioButton("Sad"), QRadioButton("Angry")]
# Set a radio button to be checked by default
moods[0].setChecked(True)
# Radio buttons usually are in a vertical layout
button_layout = QVBoxLayout()
# Create a button group for radio buttons
self.mood_button_group = QButtonGroup()
for i in xrange(len(moods)):
# Add each radio button to the button layout
button_layout.addWidget(moods[i])
# Add each radio button to the button group & give it an ID of i
self.mood_button_group.addButton(moods[i], i)
# Connect each radio button to a method to run when it's clicked
self.connect(moods[i], SIGNAL("clicked()"), self.radio_button_clicked)
# Set the layout of the group box to the button layout
self.setLayout(button_layout)
#Print out the ID & text of the checked radio button
def radio_button_clicked(self):
print(self.mood_button_group.checkedId())
print(self.mood_button_group.checkedButton().text())
app = QApplication(sys.argv)
mood_example = MoodExample()
mood_example.show()
sys.exit(app.exec_())
I found more information at:
http://codeprogress.com/python/libraries/pyqt/showPyQTExample.php?index=387&key=QButtonGroupClick
http://www.pythonschool.net/pyqt/radio-button-widget/
I want to change the label of the 'Apply' button of a gtk.Assistant to 'Start'. I can't find the corresponding gtk.Button widget in the Assistant instance.
Here's some basic code for a two-page Assistant:
import gtk
a = gtk.Assistant()
page = gtk.CheckButton("Something optional")
a.append_page(page)
a.set_page_type(page, gtk.ASSISTANT_PAGE_CONTENT)
a.set_page_title(page, "Make decisions")
a.set_page_complete(page, True)
page = gtk.Label("Alright, let's build some foo.")
a.append_page(page)
a.set_page_type(page, gtk.ASSISTANT_PAGE_CONFIRM)
a.set_page_title(page, "Confirm")
a.set_page_complete(page, True)
a.connect('delete-event', gtk.main_quit)
a.connect('close', gtk.main_quit)
a.show_all()
gtk.main()
On the final page you'll see the 'Apply' button. I want to change that text to 'Start'.
gtk.Assistant.children() and .get_children() return the list of page widgets.
gtk.Assistant.get_child() returns None.
gtk.Assistant.get_action_area() isn't a method.
Here's a link to the documentation.: http://www.pygtk.org/docs/pygtk/class-gtkassistant.html
How do I find the gtk.Button I'm interested in?
I managed to find a solution while experimenting with workarounds.
gtk.Assistant overrides the gtk.Container.get_children() method with something that returns the list of pages, but it is still in fact the parent of a gtk.HBox() which contains the buttons for 'Next', 'Apply', 'Cancel', etc.
The method gtk.Assistant.add_action_widget() adds a widget to the so-called "action area". It turns out this is the HBox containing the relevant buttons. The following function will produce a reference to the HBox:
def get_buttons_hbox(assistant):
# temporarily add a widget to the action area and get its parent
label = gtk.Label('')
assistant.add_action_widget(label)
hbox = label.get_parent()
hbox.remove(label)
return hbox
Then the buttons are retrieved using get_buttons_hbox(a).get_children().
for child in get_buttons_hbox(a).get_children():
print child.get_label()
This prints:
gtk-goto-last
gtk-go-back
gtk-go-forward
gtk-apply
gtk-cancel
gtk-close
So the following code solves the problem (using get_buttons_hbox() defined above):
for child in get_buttons_hbox(a).get_children():
label = child.get_label()
if label == 'gtk-apply':
child.set_label('Start')
I'm not sure this will be possible with pygtk. If you switch to GObject Introspection with python you can set a fully custom action area. From the Gtk3 GtkAssistant documentation:
If you have a case that doesn't quite fit in GtkAssistants way of
handling buttons, you can use the GTK_ASSISTANT_PAGE_CUSTOM page type
and handle buttons yourself.
and
GTK_ASSISTANT_PAGE_CUSTOM Used for when other page types are not
appropriate. No buttons will be shown, and the application must add
its own buttons through gtk_assistant_add_action_widget().
I've made a custom dialog that contains a series of text controls. Every text control has a couple of buttons beside them for adding specific values more conveniently. I don't want these buttons to receive focus when the user it tab traversing through the dialog, since the user, in most cases, won't need to use the buttons.
Is there any convenient way to exclude specific controllers from the standard tab traversal?
A simple way to prevent a button from being focused with the keyboard is to derive from wx.lib.buttons.GenButton or wx.lib.buttons.ThemedGenButton which are based on wx.PyControl that supports overriding of AcceptsFocusFromKeyboard():
class NoFocusButton(wx.lib.buttons.ThemedGenButton):
def __init__(self, parent, id=wx.ID_ANY, label=wx.EmptyString, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, validator=wx.DefaultValidator, name=wx.ButtonNameStr):
wx.lib.buttons.ThemedGenButton.__init__(self,parent,id,label,pos,size,style,validator,name)
def AcceptsFocusFromKeyboard(self):
return False # does not accept focus
For more complex navigation rules or controls, you could handle wx.EVT_NAVIGATION_KEY and manage the navigation yourself. To get the list of windows to navigate, you can use self.GetChildren(). The index of the the currently focused window in the wx.WindowList can be obtained through .index(mywindow).
With that information, you can navigate through the list whenever the user presses the "navigation key" and set the focus to the next applicable control, skipping those that you don't want to focus.
To make navigating through the list easier, you could create a generator:
def CycleList(thelist,index,forward):
for unused in range(len(thelist)): # cycle through the list ONCE
if forward:
index = index+1 if index+1 < len(thelist) else 0
else:
index = index-1 if index-1 >= 0 else len(thelist)-1
yield thelist[index]
In the dialog, handle wx.EVT_NAVIGATION_KEY:
self.Bind(wx.EVT_NAVIGATION_KEY, self.OnNavigationKey)
def OnNavigationKey(self,event):
children = self.GetChildren() # list of child windows
focused = self.FindFocus() # current focus
# avoid accessing elements that do not exist
if not focused or focused not in children:
event.Skip() # use default behavior
return
index = children.index(focused)
for child in CycleList(children,index,event.GetDirection()):
# default behavior:
if child.AcceptsFocusFromKeyboard():
child.SetFocus()
return
The example above emulates the default behavior: it cycles through focusable controls (skipping unfocusable controls like static texts).
You could expand the check to exclude specific controls or create a custom button class that implements AcceptsFocusFromKeyboard returning False.
NOTE: While wx.PyWindow, wx.PyPanel and wx.PyControl implement the mechanism to allow overriding of AcceptsFocusFromKeyboard, the standard wxPython controls do not.
However, handling wx.EVT_NAVIGATION_KEY and checking AcceptsFocusFromKeyboard on the python side will access the actual python object which will invoke the overridden method.
If you were using C++ this problem has a straightforward solution as described in the remainder of this answer. In wxPython it seems you cannot specialize wxWidgets classes - which seems to me a fatal snag.
You could create a specialization of the button control which will be left out of the tab traversal by overriding AcceptsFocusFromKeyboard() to return FALSE.
http://docs.wxwidgets.org/trunk/classwx_window.html#a2370bdd3ab08e7ef3c7555c6aa8301b8
The following C++ code works fine: focus jumps from the first to the third button when tab is pressed
class cNoTabButton : public wxButton
{
public:
cNoTabButton(wxWindow *parent,
wxWindowID id,
const wxString& label = wxEmptyString,
const wxPoint& pos = wxDefaultPosition,
const wxSize& size = wxDefaultSize,
long style = 0 )
: wxButton(parent,id,label,pos,size,style)
{}
bool AcceptsFocusFromKeyboard() const {
return false;
}
};
MyFrame::MyFrame(const wxString& title)
: wxFrame(NULL, wxID_ANY, title)
{
// set the frame icon
SetIcon(wxICON(sample));
wxPanel * panel = new wxPanel(this,-1,wxPoint(0,0),wxSize(500,500));
new wxButton(panel,-1,"TabPlease",wxPoint(20,20));
new cNoTabButton(panel,-1,"NoTabThanks",wxPoint(100,20));
new wxButton(panel,-1,"OKWithMe",wxPoint(200,20));
}
This is not a perfect solution but one way to do this is actually to delay your control's getting the focus back by half a second.
Your main window will get focus back and buttons still work. I use it because I want my main windows to handle all key presses but still to contain buttons on it that are used with the mouse.
So you bind your KILL_FOCUS event in the control that is supposed to preserve the focus and you create a list of controls that cannot get the focus from it.
First a helper function to get all children:
def GetAllChildren(control):
children = []
for c in control.GetChildren():
children.append(c)
children += GetAllChildren(c)
return children
In my case I want all the children of the window to not get focus
self.not_allowed_focus = GetAllChildren(self)
self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
In my KILL FOCUS handler I ask for the focus back after half a second
def OnKillFocus(self,evt):
print "OnKillFocus"
win = evt.GetWindow()
if win in self.not_allowed_focus:
wx.CallLater(500,self.SetFocus)