My objective is to give the user QComboBoxes depending on the number he/she selected on the QSpinbox.
So, it will be something like this:
User will select a number on QSpinbox,after that, by using the signal valueChanged(int) i want to invoke another function that will create QComboBoxes for user to enter the data.
As soon as user change to another value. It will automatically increase / decrease the QComboBox numbers depending on the QSpinbox value.
So i came up with something like this:
def myfunction(x):
labellist = []
combolist = []
layoutlist = []
layout = QtGui.QVBoxLayout()
for i in range(x):
labellist.append(QtGui.QLabel('this is label'+str(i))
combolist.append(QtGui.QComboBox())
layoutlist.append(QtGui.QHBoxLayout())
layoutlist[i].addWidget(labellist[i])
layoutlist[i].addWidget(combolist[i])
layout.addLayout(layoutlist[i])
self.connect(number,QtCore.SIGNAL("valueChanged(int)"),myfunction)
Even though it create ComboBoxes depending on the number user selected on spin box, when user increase the number (eg. 3 to 4), it doesn't remove the old 3comboBoxes, instead it become 7 comboboxes all together.
How do i fix this? Or Is there a better way to achieve the similar result?
You could delete and recreate all the comboboxes everytime your spinbox value changes. It may not be the most efficient, but it's quite simple.
Just have a list with references to the labels/comboxes as an attribute. In your slot, call deleteLater() on each item, then delete the reference by setting your list to []. Finally, recreate the items, add them to your layout and repopulate your list.
Also, you should have a look at New Style Signals and Slots. They are nicer than the ugly C++ style connect.
class DynamicComboBoxes(QtGui.QWidget):
def __init__(self, parent=None):
super(DynamicComboBoxes, self).__init__(parent)
vbox = QtGui.QVBoxLayout(self)
spinbox = QtGui.QSpinBox(self)
spinbox.setRange(0,10)
spinbox.valueChanged.connect(self.onChangeValue)
vbox.addWidget(spinbox)
self.grid = QtGui.QGridLayout()
self.itemlist = []
vbox.addLayout(self.grid)
vbox.addStretch(1)
def onChangeValue(self, val):
for label, combobox in self.itemlist:
label.deleteLater()
combobox.deleteLater()
self.itemlist = []
for i in range(val):
label = QtGui.QLabel('This is Label {}'.format(i))
combobox = QtGui.QComboBox()
self.grid.addWidget(label, i, 0)
self.grid.addWidget(combobox, i, 1)
self.itemlist.append([label, combobox])
Related
I created different tabs using QTableWidget in a loop:
self.table_widget = QTableWidget()
for i in range(0, 5):
self.table_widget.tab = QWidget()
self.table_widget.tabs.addTab(self.table_widget.tab, f"{i}")
self.table_widget.tab.Layout = QVBoxLayout(self)
self.tab_table = QTableWidget(self)
self.table_widget.tab.Layout.addWidget(self.tab_table)
self.table_widget.tab.setLayout(self.table_widget.tab.Layout)
self.table_widget.show()
How can I access the QTableWidget inside the different tabs? Each tab does not have a specific variable name, which can be used to control the widget. I can get the currentIndex of the tab, but I don't know how to use this information.
The simple solution is to create a list as an instance attribute, and add the table widgets to it. Note that there's no point in setting instance attributes in a loop, as they will be overwritten every time.
Also, you should not create a layout with a widget argument if you're going to set that layout to another widget.
self.tables = []
for i in range(5):
tab = QWidget()
self.table_widget.tabs.addTab(tab, f"{i}")
layout = QVBoxLayout(tab)
tab_table = QTableWidget()
layout.addWidget(tab_table)
self.tables.append(tab_table)
Alternatively, you can make the table an instance member of the page:
for i in range(5):
tab = QWidget()
self.table_widget.tabs.addTab(tab, f"{i}")
layout = QVBoxLayout(tab)
tab.tab_table = QTableWidget()
layout.addWidget(tab.tab_table)
def get_table(self, index):
tab = self.table_widget.tabs.widget(index)
return tab.tab_table
Another possibility, as long as it's guaranteed that each tab only has one table, is to use findChild():
def get_table(self, index):
tab = self.table_widget.tabs.widget(index)
return tab.findChild(QTableWidget)
Is it possible to have a multi-line text entry field with drop down options?
I currently have a GUI with a multi-line Text widget where the user writes some comments, but I would like to have some pre-set options for these comments that the user can hit a drop-down button to select from.
As far as I can tell, the Combobox widget does not allow changing the height of the text-entry field, so it is effectively limited to one line (expanding the width arbitrarily is not an option). Therefore, what I think I need to do is sub-class the Text widget and somehow add functionality for a drop down to show these (potentially truncated) pre-set options.
I foresee a number of challenges with this route, and wanted to make sure I'm not missing anything obvious with the existing built-in widgets that could do what I need.
Terry's feedback made it clear that there was no simple way to solve this, so I created a custom class which wraps a Text and a Button into a frame, with a Toplevel containing a Listbox spawned by the button's callback function. I added a couple of "nice-to-have" features, like option highlighting within the Listbox, and I mapped bindings of the main widget onto the internal Text widget to make it easier to work with. Please leave a comment if there's any glaring bad practices here; I'm definitely still pretty inexperienced! But I hope this helps anybody else who's looking for a multi-line combobox!
class ComboText(tk.Frame):
def __init__(self, parent=None, **kwargs):
super().__init__(parent)
self.parent = parent
self._job = None
self.data = []
self['background'] = 'white'
self.text = tk.Text(self, **kwargs)
self.text.pack(side=tk.LEFT, expand=tk.YES, fill='x')
symbol = u"\u25BC"
self.button = tk.Button(self,width = 2,text=symbol, background='white',relief = 'flat', command = self.showOptions)
self.button.pack(side=tk.RIGHT)
#pass bindings from parent frame widget to the inner Text widget
#This is so you can bind to the main ComboText and have those bindings
#apply to things done within the Text widget.
#This could also be applied to the inner button widget, but since
#ComboText is intended to behave "like" a Text widget, I didn't do that
bindtags = list(self.text.bindtags())
bindtags.insert(0,self)
self.text.bindtags(tuple(bindtags))
def showOptions(self):
#Get the coordinates of the parent Frame, and the dimensions of the Text widget
x,y,width,height = [self.winfo_rootx(), self.winfo_rooty(), self.text.winfo_width(), self.text.winfo_height()]
self.toplevel = tk.Toplevel()
self.toplevel.overrideredirect(True) #Use this to get rid of the menubar
self.listbox = tk.Listbox(self.toplevel,width=width, height =len(self.data))
self.listbox.pack()
#Populate the options in the listbox based on self.data
for s in self.data:
self.listbox.insert(tk.END,s)
#Position the Toplevel so that it aligns well with the Text widget
list_height = self.listbox.winfo_reqheight()
self.toplevel.geometry("%dx%d+%d+%d" % (width, list_height, x, y+height))
self.listbox.focus_force()
self.listbox.bind("<Enter>", self.ListboxHighlight)
self.listbox.bind("<Leave>",self.stopListboxHighlight)
self.listbox.bind("<Button-1>",self.selectOption)
self.toplevel.bind("<Escape>", self.onCancel)
self.toplevel.bind("<FocusOut>", self.onCancel)
def ListboxHighlight(self,*ignore):
#While the mouse is moving within the listbox,
#Highlight the option the mouse is over
x,y = self.toplevel.winfo_pointerxy()
widget = self.toplevel.winfo_containing(x,y)
idx = self.listbox.index("#%s,%s" % (x-self.listbox.winfo_rootx(),y-self.listbox.winfo_rooty()))
self.listbox.selection_clear(0,100) #very sloppy "Clear all"
self.listbox.selection_set(idx)
self.listbox.activate(idx)
self._job = self.after(25,self.ListboxHighlight)
def stopListboxHighlight(self,*ignore):
#Stop the recurring highlight function.
if self._job:
self.after_cancel(self._job)
self._job = None
def onCancel(self,*ignore):
#Stop callback function to avoid error once listbox destroyed.
self.stopListboxHighlight()
#Destroy the popup Toplevel
self.toplevel.destroy()
def selectOption(self,event):
x,y = [event.x,event.y]
idx = self.listbox.index("#%s,%s" % (x,y))
if self.data:
self.text.delete('1.0','end')
self.text.insert('end',self.data[idx])
self.stopListboxHighlight()
self.toplevel.destroy()
self.text.focus_force()
def setOptions(self,optionList):
self.data = optionList
#Map the Text methods onto the ComboText class so that
#the ComboText can be treated like a regular Text widget
#with some other options added in.
#This was necessary because ComboText is a subclass of Frame, not Text
def __getattr__(self,name):
def textMethod(*args, **kwargs):
return getattr(self.text,name)(*args, **kwargs)
return textMethod
if __name__ == '__main__':
root = tk.Tk()
ct = ComboText(root, width = 50, height = 3)
ct.pack()
ct.setOptions(['Option %d' % i for i in range (0,5)])
root.mainloop()
I don't think you are missing anything. Note that ttk.Combobox is a composite widget. It subclasses ttk.Entry and has ttk.Listbox attached.
To make multiline equivalent, subclass Text. as you suggested. Perhaps call it ComboText. Attach either a frame with multiple read-only Texts, or a Text with multiple entries, each with a separate tag. Pick a method to open the combotext and methods to close it, with or without copying a selection into the main text. Write up an initial doc describing how to operate the thing.
How to make a list of line editors without many variables? (smth like self.line_1 = QLineEdit(self), self.line_2 = QLineEdit(self), ... , self.line_9000 = QLineEdit(self))
For example, I want to create this
window with ability to get access to each element.
A simple cycle does not provide access to each element, only last. How I can do this?
One way is to make widgets as you said - cycle,
and you can access to the widget with using layout.itemAtPosition
it would go like this :
layout = QVBoxLayout()
for i in range(list_length):
line_edit = QLineEdit(self)
layout.addWidget(line_edit)
to access the widget :
def access_widget(int):
item = layout.itemAtPosition(int)
line_edit = item.widget()
return line_edit
now you can access to the designated QLineEdit.
layout = QFormLayout()
self.alphabet_line_edits = dict.fromkeys(['а', 'б', 'в', 'г'])
for letter in self.alphabet_line_edits:
line_edit = QLineEdit()
layout.addRow(letter, line_edit)
self.alphabet_line_edits[letter] = line_edit
def button_clicked(self):
print(self.alphabet_line_edit['б'].text())
I am creating labels in a for loop that display integers every time I fire an event (a mouse click) on my application. The problem is that old labels don't get erased and the new ones come on top of them causing a big mess.
Here is the working code that you can try out:
import numpy as np
import Tkinter as tk
class Plot(object):
def __init__(self, win):
self.win = win
self.bu1 = tk.Button(win,text='Load',command=self.populate,fg='red').grid(row=0,column=0)
self.listbox = tk.Listbox(win, height=5, width=5)
self.listbox.grid(row=1,column=0)#, rowspan=10, columnspan=2)
self.listbox.bind("<Button-1>", self.print_area)
def populate(self):
"""Populate listbox and labels"""
self.time = [1,2,3]
self.samples = ['a','b','c']
for item in self.time:
self.listbox.insert(tk.END,item)
for i,v in enumerate(self.samples):
tk.Label(self.win, text=v).grid(row=2+i,column=0,sticky=tk.W)
self.lbl_areas = []
for i in range(0, len(self.samples)):
self.lbl=tk.IntVar()
self.lbl.set(0)
self.lbl_areas.append(tk.Label(self.win,textvariable=self.lbl).grid(row=2+i,column=1,sticky=tk.W))
def print_area(self, event):
"""Prints the values"""
widget = event.widget
selection=widget.curselection()
value = widget.get(selection[0])
#Here is the dictionary that maps time with values
self.d = {1:[('a',33464.1),('b',43.5),('c',64.3)],
2:[('a',5.1),('b',3457575.5),('c',25.3)],
3:[('a',12.1),('b',13.5),('c',15373.3)]}
lbl_val = []
for i in range(0, len(self.samples)):
lbl_val.append(self.d[value][i][1])
for i in range(0, len(self.samples)):
self.lbl=tk.IntVar()
self.lbl.set(lbl_val[i])
tk.Label(self.win,textvariable=self.lbl).grid(row=2+i,column=1,sticky=tk.W)
def main():
root = tk.Tk()
app = Plot(root)
tk.mainloop()
if __name__ == '__main__':
main()
If You try to run this code and click on LOAD you will see the numbers appearing in the listbox and labels a,b,c with values set to zero at the beginning. If you click on the number in the listbox the values (mapped into the dictionary d) will appear but you will see the overwrite problem. How can I fix that?
How can I overcome this problem? Thank you
Don't create new labels. Create the labels once and then update them on mouse clicks using the configure method of the labels.
OR, before creating new labels delete the old labels.If you design your app so that all of these temporary labels are in a single frame you can delete and recreate the frame, and all of the labels in the frame will automatically get deleted. In either case (destroying the frame or destroying the individual labels) you would call the destroy method on the widget you want to destroy.
After every click on a "Add Item..." button, I want a row(label, button) to be appended to the layout (below that same button).
So, it should add one row per click.
Problem is it adds the following:
1st click: 1 row added (total item rows = 1) (correct)
2nd click: 2 rows added (total item rows = 3) (should be 2)
3rd click: 3 rows added (total item rows = 6) (should be 3)
Here's the relevant code:
from PySide import QtCore
from PySide import QtGui
import sys
class Form(QtGui.QDialog):
items = []
def __init__(self, parent = None):
super(Form, self).__init__(parent)
self.btn = QtGui.QPushButton("Add Item...")
self.btn.clicked.connect(self.item_toggle)
self.layout = self.initial_view()
self.setLayout(self.layout)
def item_toggle(self, add = True):
layout = self.layout
if add:
string = ("25468 5263.35 54246") #####random text
self.items.append(string)
for item in self.items:
rem_btn = QtGui.QPushButton("X")
rem_btn.clicked.connect(self.remove_item)
layout.addRow(item, rem_btn)
self.setLayout(layout)
def remove_item(self, ):
#self.items.pop() #something to delete that item
self.add_item("False") #redraw items part
def initial_view(self, ):
layout = QtGui.QFormLayout()
#adding to layout
layout.addRow(self.btn)
return layout
app = QtGui.QApplication(sys.argv)
form = Form()
form.show()
app.exec_()
I figure its not erasing the previous widgets, but I can't quiet figure it out. Also, a way to to remove the items(remove_item function), would also help me out.
I hope I explained well and you get what I'm trying to do...
Any help will be appreciated. Thanks in advance
To prevent adding additional items to your list just remove the for loop and just do the following:
rem_btn = QtGui.QPushButton("X")
rem_btn.clicked.connect(self.remove_item)
layout.addRow(string, rem_btn)
What you have to know about the addRow call, is that this add your QPushButton in the second column, and auto-creates a QLabel for the first column. So when you want to remove the row, you will have to remove both the button and the label.
Now about the remove. I guess the easiest way to start would be to find out which button is asking to be removed.
sending_button = self.sender()
At this point you will need to get access to the QLabel. Luckily there is a call on the layout called labelForField which will return the QLabel associated with your QPushButton
labelWidget = self.layout.labelForField(sending_button)
Then to remove the actual widgets
sending_button.deleteLater()
if labelWidget:
labelWidget.deleteLater()