I have two lots of computer names found in AD, all of which are sorted together and entered into a tkinter drop-down combobox. I would like to be able to change the text colour of the entries in the drop-down depending on which original list of computers it came from.
OPSpclist = []
OPS_pcs = active_directory.AD_object ("LDAP://OU=Locations - ...")
for OPSpc in OPS_pcs.search (objectCategory='Computer'):
OPSpc = str(OPSpc).upper()
OPSpc = OPSpc.split(",")[0].split("=")[1]
OPSpclist.append(OPSpc)
OSpclist = []
OS_pcs = active_directory.AD_object ("LDAP://OU=Locations - ...")
for OSpc in OS_pcs.search (objectCategory='Computer'):
OSpc = str(OSpc).upper()
OSpc = OSpc.split(",")[0].split("=")[1]
OSpclist.append(OSpc)
bothSchools = sorted(OSpclist) + sorted(OPSpclist)
optionList = sorted(bothSchools)
var1 = StringVar()
var1.set(optionList[0])
pcnameEntry = ttk.Combobox(entryframe, textvariable = var1, values = optionList, width=25)
pcnameEntry.focus_set()
pcnameEntry.grid(row=1, column=0, sticky=W, pady=(0, 10), padx=5)
Is it possible to have the items from the first list to appear in a different colour, all within the same, sorted, combobox drop-down list?
Thanks,
Chris.
Yes this is possible, the drop down is a listbox and therefore the items can be configured separately with the itemconfigure method. However, I don't know how to retrieve the combobox's listbox via Python but this can be done through tcl commands:
import Tkinter as tk
import ttk
root = tk.Tk()
l1 = [(name, 'computer 1') for name in 'ABCD']
l2 = [(name, 'computer 2') for name in 'ACEG']
l = sorted(l1 + l2)
combo = ttk.Combobox(root, values=[name for name, computer in l])
combo.pack()
combo.update_idletasks()
# I need to display the drop down once before setting the items' colors otherwise
# I get an error telling me the items don't exist, so I generate a click on the combobox button
combo.event_generate('<1>', x=combo.winfo_width() - 4, y=1)
colors = {'computer 1': 'blue', 'computer 2': 'red'}
# define the tcl variable corresponding to the drop down listbox
combo.tk.eval('set popdown [ttk::combobox::PopdownWindow %s]' % combo)
for i, (name, computer) in enumerate(l):
# set the color of each item (the background color can be changed too by adding '-background <color>')
combo.tk.eval('$popdown.f.l itemconfigure %i -foreground %s' % (i, colors[computer]))
root.mainloop()
Related
I am trying to populate a Tkinter combobox with pre-defined values to select from. It is populating and I am able to type in and get suggestions. However, in order to do this I have to definitely know the first few characters. If I know some text in the middle or end of the string, its of no use because the combobox does only a 'LIKE%' search and not a '%LIKE%' search.
Expected Output (Typing the word "Ceramic" fetches all names containing the string. Note: This is not a Tkinter screenshot):
This is my adaptation of the code till now, if anyone can suggest how to modify the AutocompleteCombobox class to do a LIKE search, it would be great.
The below working piece of code, as an example, has values "Cranberry" and "Strawberry" , my requirement is to type "berry" and get suggestions of both fruits.
import Tkinter
import ttk
import sqlite3
class AutocompleteCombobox(ttk.Combobox):
def set_completion_list(self, completion_list):
"""Use our completion list as our drop down selection menu, arrows move through menu."""
self._completion_list = sorted(completion_list, key=str.lower) # Work with a sorted list
self._hits = []
self._hit_index = 0
self.position = 0
self.bind('<KeyRelease>', self.handle_keyrelease)
self['values'] = self._completion_list # Setup our popup menu
def autocomplete(self, delta=0):
"""autocomplete the Combobox, delta may be 0/1/-1 to cycle through possible hits"""
if delta: # need to delete selection otherwise we would fix the current position
self.delete(self.position, Tkinter.END)
else: # set position to end so selection starts where textentry ended
self.position = len(self.get())
# collect hits
_hits = []
for element in self._completion_list:
if element.lower().startswith(self.get().lower()): # Match case insensitively
_hits.append(element)
# if we have a new hit list, keep this in mind
if _hits != self._hits:
self._hit_index = 0
self._hits=_hits
# only allow cycling if we are in a known hit list
if _hits == self._hits and self._hits:
self._hit_index = (self._hit_index + delta) % len(self._hits)
# now finally perform the auto completion
if self._hits:
self.delete(0,Tkinter.END)
self.insert(0,self._hits[self._hit_index])
self.select_range(self.position,Tkinter.END)
def handle_keyrelease(self, event):
"""event handler for the keyrelease event on this widget"""
if event.keysym == "BackSpace":
self.delete(self.index(Tkinter.INSERT), Tkinter.END)
self.position = self.index(Tkinter.END)
if event.keysym == "Left":
if self.position < self.index(Tkinter.END): # delete the selection
self.delete(self.position, Tkinter.END)
else:
self.position = self.position-1 # delete one character
self.delete(self.position, Tkinter.END)
if event.keysym == "Right":
self.position = self.index(Tkinter.END) # go to end (no selection)
if len(event.keysym) == 1:
self.autocomplete()
# No need for up/down, we'll jump to the popup
# list at the position of the autocompletion
def test(test_list):
"""Run a mini application to test the AutocompleteEntry Widget."""
root = Tkinter.Tk(className='AutocompleteCombobox')
combo = AutocompleteCombobox(root)
combo.set_completion_list(test_list)
combo.pack()
combo.focus_set()
# I used a tiling WM with no controls, added a shortcut to quit
root.bind('<Control-Q>', lambda event=None: root.destroy())
root.bind('<Control-q>', lambda event=None: root.destroy())
root.mainloop()
if __name__ == '__main__':
test_list = ('apple', 'banana', 'Cranberry', 'dogwood', 'alpha', 'Acorn', 'Anise', 'Strawberry' )
test(test_list)
I suspect you need
if self.get().lower() in element.lower():
instead of
if element.lower().startswith(self.get().lower()):
to get data like with %LIKE% in database
But I don't know if you get good effect because this Combobox replaces text with suggestion so if you type be then it finds Cranberry and put in place be and you can't write ber.
Maybe you should display Cranberry as separated (dropdown) list, or popup tip.
Or maybe you will have to use string.find() to highlight correct place in Cranberry and continue to type ber in correct place.
EDIT: example how to use Entry and Listbox to display filtered list
In listbox_update I added sorting list (comparing lower case strings)
#!/usr/bin/env python3
import tkinter as tk
def on_keyrelease(event):
# get text from entry
value = event.widget.get()
value = value.strip().lower()
# get data from test_list
if value == '':
data = test_list
else:
data = []
for item in test_list:
if value in item.lower():
data.append(item)
# update data in listbox
listbox_update(data)
def listbox_update(data):
# delete previous data
listbox.delete(0, 'end')
# sorting data
data = sorted(data, key=str.lower)
# put new data
for item in data:
listbox.insert('end', item)
def on_select(event):
# display element selected on list
print('(event) previous:', event.widget.get('active'))
print('(event) current:', event.widget.get(event.widget.curselection()))
print('---')
# --- main ---
test_list = ('apple', 'banana', 'Cranberry', 'dogwood', 'alpha', 'Acorn', 'Anise', 'Strawberry' )
root = tk.Tk()
entry = tk.Entry(root)
entry.pack()
entry.bind('<KeyRelease>', on_keyrelease)
listbox = tk.Listbox(root)
listbox.pack()
#listbox.bind('<Double-Button-1>', on_select)
listbox.bind('<<ListboxSelect>>', on_select)
listbox_update(test_list)
root.mainloop()
At start with full list
Later only with filtered items
EDIT: 2020.07.21
If you want to use <KeyPress> then you have to change on_keyrelease and use event.char, event.keysym and/or event.keycode because KeyPress is executed before tkinter update text in Entry and you have to add event.char to text in Entry (or remove last char when you press backspace)
if event.keysym == 'BackSpace':
value = event.widget.get()[:-1] # remove last char
else:
value = event.widget.get() + event.char # add new char at the end
It may need other changes for other special keys Ctrl+A, Ctrl+X, Ctrl+C, Ctrl+E, etc. and it makes big problem.
#!/usr/bin/env python3
import tkinter as tk
def on_keypress(event):
print(event)
print(event.state & 4) # Control
print(event.keysym == 'a')
# get text from entry
if event.keysym == 'BackSpace':
# remove last char
value = event.widget.get()[:-1]
elif (event.state & 4): # and (event.keysym in ('a', 'c', 'x', 'e')):
value = event.widget.get()
else:
# add new char at the end
value = event.widget.get() + event.char
#TODO: other special keys
value = value.strip().lower()
# get data from test_list
if value == '':
data = test_list
else:
data = []
for item in test_list:
if value in item.lower():
data.append(item)
# update data in listbox
listbox_update(data)
def listbox_update(data):
# delete previous data
listbox.delete(0, 'end')
# sorting data
data = sorted(data, key=str.lower)
# put new data
for item in data:
listbox.insert('end', item)
def on_select(event):
# display element selected on list
print('(event) previous:', event.widget.get('active'))
print('(event) current:', event.widget.get(event.widget.curselection()))
print('---')
# --- main ---
test_list = ('apple', 'banana', 'Cranberry', 'dogwood', 'alpha', 'Acorn', 'Anise', 'Strawberry' )
root = tk.Tk()
entry = tk.Entry(root)
entry.pack()
entry.bind('<KeyPress>', on_keypress)
listbox = tk.Listbox(root)
listbox.pack()
#listbox.bind('<Double-Button-1>', on_select)
listbox.bind('<<ListboxSelect>>', on_select)
listbox_update(test_list)
root.mainloop()
BTW:
You can also use textvariable in Entry with StringVar and trace which executes function when StringVar changes content.
var_text = tk.StringVar()
var_text.trace('w', on_change)
entry = tk.Entry(root, textvariable=var_text)
entry.pack()
#!/usr/bin/env python3
import tkinter as tk
def on_change(*args):
#print(args)
value = var_text.get()
value = value.strip().lower()
# get data from test_list
if value == '':
data = test_list
else:
data = []
for item in test_list:
if value in item.lower():
data.append(item)
# update data in listbox
listbox_update(data)
def listbox_update(data):
# delete previous data
listbox.delete(0, 'end')
# sorting data
data = sorted(data, key=str.lower)
# put new data
for item in data:
listbox.insert('end', item)
def on_select(event):
# display element selected on list
print('(event) previous:', event.widget.get('active'))
print('(event) current:', event.widget.get(event.widget.curselection()))
print('---')
# --- main ---
test_list = ('apple', 'banana', 'Cranberry', 'dogwood', 'alpha', 'Acorn', 'Anise', 'Strawberry' )
root = tk.Tk()
var_text = tk.StringVar()
var_text.trace('w', on_change)
entry = tk.Entry(root, textvariable=var_text)
entry.pack()
listbox = tk.Listbox(root)
listbox.pack()
#listbox.bind('<Double-Button-1>', on_select)
listbox.bind('<<ListboxSelect>>', on_select)
listbox_update(test_list)
root.mainloop()
I am trying to develop a script that allows me to keep my formatting within my listbox.
from Tkinter import *
from tabulate import tabulate
master = Tk()
listbox = Listbox(master)
listbox.pack()
table = [["spam",42],["eggs",451],["bacon",0]]
headers = ["item", "qty"]
tb = tabulate(table, headers, tablefmt="plain")
listbox.insert(END,tb)
mainloop()
End Result the listbox populated with the tb formatting:
QUESTION: HOW DO I GET MY LISTBOX TO APPEAR LIKE THE PICTURE ABOVE THAT I USED TABULATE TO FORMAT?
I've noticed treeview seems to have some limitations with the horizontal box and expanding the columns without adjusting the entire GUI so I'd decided this might be a more shake-shift way that will suit my needs just fine.
One option may be to use str.format() to align each insert into the listbox:
from Tkinter import *
import tkFont
master = Tk()
master.resizable(width=False, height=False)
master.geometry('{width}x{height}'.format(width=300, height=100))
my_font = tkFont.Font(family="Monaco", size=12) # use a fixed width font so columns align
listbox = Listbox(master, width=400, height=400, font=my_font)
listbox.pack()
table = [["spam", 42, "test", ""],["eggs", 451, "", "we"],["bacon", "True", "", ""]]
headers = ["item", "qty", "sd", "again"]
row_format ="{:<8}{sp}{:>8}{sp}{:<8}{sp}{:8}" # left or right align, with an arbitrary '8' column width
listbox.insert(0, row_format.format(*headers, sp=" "*2))
for items in table:
listbox.insert(END, row_format.format(*items, sp=" "*2))
mainloop()
Which appears to match the output you got using tabulate:
Another option could be use a Grid layout.
Win7 SP1, Python 2.7,Tkinter.
I make a frame, and I place an array of labels within. The array is 10w x 24h.
I want to click on one of these labels and return the name of the label so I may alter it's variable.
I know how to do using a listbox, but how to do using just 'label in a frame'?
Thanks, Mark.
You don't need the label name, and besides labels don't have useful names. When the event fires you are given a reference to the widget, which you can use to query or modify the widget attributes.
Here's an example of how you can change a label by clicking on it. Run the program, and then click on any label as often as you want.
import Tkinter as tk
import time
def on_click(event):
now = time.strftime("%H:%M:%S")
event.widget.configure(text="you clicked me at %s" % now)
root = tk.Tk()
for row in range(4):
for col in range(4):
label = tk.Label(root, width=25, borderwidth=1, relief="sunken")
label.grid(row=row, column=col, padx=2, pady=2)
label.bind("<1>", on_click)
root.mainloop()
Total pyqt novice here. Trying to automatically modify the widget to display different options when user selects certain option in combobox 2. IE if user selects 'Cliff Erosion' or 'Dune Erosion', I want the widget to refresh with additional comboboxes. Likewise, if they select back to 'Rising Tides' or 'Coastal Flooding' I'd like the widget to go back to original call. How do I refresh the widget?
from PyQt4.QtGui import *
# Create window
class Window(QWidget):
#This block adds features into the window init
def __init__(self):
QWidget.__init__(self)
self.setWindowTitle('Monterey Bay Sea Level Rise')
self.resize(300, 240)
self.addWidgets1()
def addWidgets1(self):
#Add drop-down list for selecting forecast year
self.year_lbl = QLabel("1. Select Forecast Year", self)
self.year_lbl.move(5,0)
year = QComboBox(self)
year.addItem('2030')
year.addItem('2060')
year.addItem('2100')
year.move(5,20)
#Add drop-down list for selecting hazard
self.hazard_lbl = QLabel("2. Select Coastal Hazard", self)
self.hazard_lbl.move(5,50)
hazard = QComboBox(self)
hazard.addItem('Rising Tides')
hazard.addItem('Coastal Storm Flooding')
hazard.addItem('Cliff Erosion')
hazard.addItem('Dune Erosion')
hazard.activated[str].connect(self.addWidget2)
hazard.move(5,70)
#Add drop-down list for inputing model intensity (s1,s2,s3)
self.intensity_lbl = QLabel("3. Select Intensity", self)
self.intensity_lbl.move(5,100)
intensity = QComboBox(self)
intensity.addItem('Low')
intensity.addItem('Mid')
intensity.addItem('High')
intensity.move(5,120)
def addWidget2(self,text):
#if hazard is cliff erosion or dune erosion we want to update the widget
#... to include wstorm,long_term AND no_change,stormier
if text == 'Cliff Erosion' or text == 'Dune Erosion':
print 'Hi'
self.type_lbl = QLabel("3. Select type of changes", self)
self.type_lbl.move(5,150)
types = QComboBox(self)
types.addItem('Long-term')
types.addItem('Storm induced')
types.move(5,180)
self.storm_lbl = QLabel("4. Select for stormier", self)
self.storm_lbl.move(5,150)
storm = QComboBox(self)
storm.addItem('No Change')
storm.addItem('Stormier')
storm.move(5,180)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = Window()
#window.resize(100, 60)
window.show()
sys.exit(app.exec_())
The normal way of adding widgets to a Qt application is to use layouts. They will calculate the preferred size and location of your widgets and update this when needed, e.g. when widgets are added or when the window is resized (note what happens when you make your window smaller during execution and compare it with my solution below). I'm certain that it's possible to do all the moving and resizing yourself, but QLayouts are really the way to go and I strongly recommend you use them too.
There are a few types of QLayouts but in your case I would use a QFormLayout. To my surprise the QFormLayout does have an addRow method but no corresponding removeRow. However I find that just showing/hiding the combo boxes when needed does the trick as well. I've adapted your example below.
Finally, even just showing or hiding the last two combo boxes will cause the layout to slightly move the first three. This is caused by the fact that the 4th label is the longest of them all. I find that annoying. Perhaps a better solution is to enable/disable the combo boxes when needed. This has the additional benefit of showing the user that these options even exist. See also the code below. A second alternative may be to use a QGridLayout (instead of a QFormLayout) and use setColumnMinimumWidth to set the first column to a size that will hold all possible labels.
from PyQt4 import QtGui
# Create window
class Window(QtGui.QWidget):
#This block adds features into the window init
def __init__(self):
QtGui.QWidget.__init__(self)
self.setWindowTitle('Monterey Bay Sea Level Rise')
self.resize(300, 240)
self.addWidgets1()
def addWidgets1(self):
self.layout = QtGui.QFormLayout()
self.setLayout(self.layout)
#Add drop-down list for selecting forecast year
# You don't need to set to parent of the widgets to self anymore, the
# layout will set the parent automatically when you add the widgets
self.year_lbl = QtGui.QLabel("1. Select Forecast Year")
# self.year_lbl.move(5,0) # Can be removed. The layout takes care of it.
year = QtGui.QComboBox()
year.addItem('2030')
year.addItem('2060')
year.addItem('2100')
self.layout.addRow(self.year_lbl, year)
#Add drop-down list for selecting hazard
self.hazard_lbl = QtGui.QLabel("2. Select Coastal Hazard")
self.hazard = QtGui.QComboBox()
self.hazard.addItem('Rising Tides')
self.hazard.addItem('Coastal Storm Flooding')
self.hazard.addItem('Cliff Erosion')
self.hazard.addItem('Dune Erosion')
self.hazard.activated[str].connect(self.updateComboboxes)
self.layout.addRow(self.hazard_lbl, self.hazard)
#Add drop-down list for inputing model intensity (s1,s2,s3)
self.intensity_lbl = QtGui.QLabel("3. Select Intensity")
intensity = QtGui.QComboBox()
intensity.addItem('Low')
intensity.addItem('Mid')
intensity.addItem('High')
self.layout.addRow(self.intensity_lbl, intensity)
self.types_lbl = QtGui.QLabel("3. Select type of changes")
self.types = QtGui.QComboBox()
self.types.addItem('Long-term')
self.types.addItem('Storm induced')
self.layout.addRow(self.types_lbl, self.types)
self.storm_lbl = QtGui.QLabel("4. Select for stormier")
self.storm = QtGui.QComboBox()
self.storm.addItem('No Change')
self.storm.addItem('Stormier')
self.layout.addRow(self.storm_lbl, self.storm)
# show initial state
self.updateComboboxes()
def updateComboboxes(self, text=None):
#if hazard is cliff erosion or dune erosion we want to update the widget
#... to include wstorm,long_term AND no_change,stormier
if text is None:
text = self.hazard.currentText()
usable = (text == 'Cliff Erosion' or text == 'Dune Erosion')
if True: # change to False to use enabling/disabling widgets
# May cause other widgets to be relocated
self.types_lbl.setVisible(usable)
self.types.setVisible(usable)
self.storm_lbl.setVisible(usable)
self.storm.setVisible(usable)
else:
# This option doesn't relocate widgets
# Also may give additional clue to the uses that this exsits
self.types_lbl.setEnabled(usable)
self.types.setEnabled(usable)
self.storm_lbl.setEnabled(usable)
self.storm.setEnabled(usable)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
#window.resize(100, 60)
window.show()
sys.exit(app.exec_())
def nextItem(self):
active = self.skill_list_listbox.get(tk.ACTIVE)
listbox_contents = self.skill_list_listbox.get(0, tk.END)
current_pos = listbox_contents.index(active)
if current_pos + 1 < len(listbox_contents):
new_pos = current_pos + 1
self.skill_list_listbox.activate(new_pos)
self.skill_list_listbox.selection_set(tk.ACTIVE)
From what I can see within documentation this should highlight and activate the next item in the listbox. If I omit the selection_set I get what I'm looking for but there's no indicator of what's active. Adding it highlights an item, but if you continue to click the "next" button it simply adds to the highlight instead of just highlighting one item creating a long section of highlighted items, which I don't want. I've tried several different methods and this has got me the closest. If there was a 'clear selection' method I suppose I could get my desired effect of just having the next item selected and highlighted, but 3 calls just to do that seems a bit much for a common task? Any thoughts, or suggestions?
Below is an example of what I think you are trying to accomplish, using a button to select the next item in a Listbox. The gist of it is in the button's callback function, which calls selection_clear then selection_set.
Updated the example, hopefully a bit clearer as to what it happening
import Tkinter
class Application(Tkinter.Frame):
def __init__(self, master):
Tkinter.Frame.__init__(self, master)
self.master.minsize(width=256, height=256)
self.master.config()
self.pack()
self.main_frame = Tkinter.Frame()
self.some_list = [
'One',
'Two',
'Three',
'Four'
]
self.some_listbox = Tkinter.Listbox(self.main_frame)
self.some_listbox.pack(fill='both', expand=True)
self.main_frame.pack(fill='both', expand=True)
# insert our items into the list box
for i, item in enumerate(self.some_list):
self.some_listbox.insert(i, item)
# add a button to select the next item
self.some_button = Tkinter.Button(
self.main_frame, text="Next", command=self.next_selection)
self.some_button.pack(side='top')
# not really necessary, just make things look nice and centered
self.main_frame.place(in_=self.master, anchor='c', relx=.5, rely=.5)
def next_selection(self):
selection_indices = self.some_listbox.curselection()
# default next selection is the beginning
next_selection = 0
# make sure at least one item is selected
if len(selection_indices) > 0:
# Get the last selection, remember they are strings for some reason
# so convert to int
last_selection = int(selection_indices[-1])
# clear current selections
self.some_listbox.selection_clear(selection_indices)
# Make sure we're not at the last item
if last_selection < self.some_listbox.size() - 1:
next_selection = last_selection + 1
self.some_listbox.activate(next_selection)
self.some_listbox.selection_set(next_selection)
root = Tkinter.Tk()
app = Application(root)
app.mainloop()