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()
Related
I'm working on a GUI to automate part of my project's pipeline using Tkinter and ttk in Python 2.7. I have a main Tk() window that generates a Toplevel() window upon clicking "Auto-Detect", then creates a dynamic series of readonly Entry widgets on button click based on a list of molecular species defined elsewhere in the code.
My issue is that, while the Entry boxes do appear correctly based on the species detected, they do not stay populated with the species names once the function that created them terminates. I suspect this is because the widgets aren't global [or even within the scope of the Tk() window], but rather are defined only within the function. However, I can't create them within the block of code where the Tk() window is defined since the number of Entry boxes needed is unknown until the button is pressed (thus calling the function that creates them).
I've included an abstracted block of code below that shows the problem I'm having. Apologies if it is long. I tried to cut it down as much as possible. The comments I included in the code show my thoughts and guesses for what's going on. It should be ready to run in Python 2.7; my hope is that the only Python 3.x changes that are necessary are import modifications.
My question is, after I have dynamically created Entry widgets within a function that is called by the main Tk() window and populated them with text, how can I prevent them from depopulating when the function ends? Disclaimer: I'm not a programmer (or even in the field of computer science) by trade, so I will do my best to hang on to all technical details, but I may have to ask some dumb questions.
from Tkinter import *
import ttk
from time import sleep
def update_list(manager, rows):
species_list = ['A','B','C','D']
del rows[1:]
for widget in manager.children.values():
widget.grid_forget()
if not species_list == ['']:
species_elem_list = []
for i, each in enumerate(species_list):
## Here I attempt to create a dynamic list of StringVars to attach to the Entry fields below, based on the contents of the species list.
species_elem_list.append(StringVar())
## Here I initialize the values of the elements of the Entry fields by setting the StringVar of each.
species_elem_list[i].set(each)
## I tried to attach the value of the StringVar (from the species list) to the Entry below, but when the program is run, the Entry does not stay populated.
temp_name = ttk.Entry(manager, textvariable=species_elem_list[i], state='readonly')
temp_color = ttk.Label(manager, text='data')
temp_row = [temp_name, temp_color]
rows.append(temp_row)
for row_number in range(len(rows)):
for column_number, each in enumerate(rows[row_number]):
each.grid(column=column_number, row=row_number)
each.grid()
manager.update()
sleep(3) ## Included so that the population of the fields can be observed before they depopulate.
## After this point, the update_list function terminates.
## When that happens, the Entry fields depopulate. How can I get them to persist after the function terminates?
root = Tk()
manager = ttk.Frame(root, padding='4 5 4 4')
manager.grid(column=0, row=0, sticky=NSEW)
name_label = ttk.Label(manager, text='Name')
color_label = ttk.Label(manager, text='RGB')
rows = [[name_label, color_label]]
options = ttk.Frame(root)
options.grid(sticky=NSEW)
detect_button = ttk.Button(options, text='Auto-Detect', command=lambda: update_list(manager,rows))
done_button = ttk.Button(options, text='Done', command=root.destroy)
detect_button.grid(column=0, row=0)
done_button.grid(column=1, row=0)
root.mainloop()
Ideally, the Entry widgets will remain (and remain populated!) after the function update_list terminates. I also want to be able to interact with the contents of these widgets from outside the function.
Currently, the Entry fields populate during the course of the function update_list, then depopulate immediately once it ends. I suspect this is because the widgets and their contents are not global in scope.
In textvariable=species_elem_list you use local variable species_elem_list which stop exist when you exit update_list()
You have to create species_elem_list outside update_list() and use global species_elem_list in update_list() to use global variable instead of local one when you do species_elem_list = []
from Tkinter import *
import ttk
from time import sleep
species_elem_list = [] # <--- put here or below of update_list
def update_list(manager, rows):
global species_elem_list # <-- use global variable instead of local one
species_list = ['A','B','C','D']
del rows[1:]
for widget in manager.children.values():
widget.grid_forget()
if not species_list == ['']:
species_elem_list = []
for i, each in enumerate(species_list):
## Here I attempt to create a dynamic list of StringVars to attach to the Entry fields below, based on the contents of the species list.
species_elem_list.append(StringVar())
## Here I initialize the values of the elements of the Entry fields by setting the StringVar of each.
species_elem_list[i].set(each)
## I tried to attach the value of the StringVar (from the species list) to the Entry below, but when the program is run, the Entry does not stay populated.
temp_name = ttk.Entry(manager, textvariable=species_elem_list[i], state='readonly')
temp_color = ttk.Label(manager, text='data')
temp_row = [temp_name, temp_color]
rows.append(temp_row)
for row_number in range(len(rows)):
for column_number, each in enumerate(rows[row_number]):
each.grid(column=column_number, row=row_number)
each.grid()
manager.update()
sleep(3) ## Included so that the population of the fields can be observed before they depopulate.
## After this point, the update_list function terminates.
## When that happens, the Entry fields depopulate. How can I get them to persist after the function terminates?
root = Tk()
manager = ttk.Frame(root, padding='4 5 4 4')
manager.grid(column=0, row=0, sticky=NSEW)
name_label = ttk.Label(manager, text='Name')
color_label = ttk.Label(manager, text='RGB')
rows = [[name_label, color_label]]
options = ttk.Frame(root)
options.grid(sticky=NSEW)
detect_button = ttk.Button(options, text='Auto-Detect', command=lambda: update_list(manager,rows))
done_button = ttk.Button(options, text='Done', command=root.destroy)
detect_button.grid(column=0, row=0)
done_button.grid(column=1, row=0)
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.
I am trying to implement an interactive dashboard in Bokeh with a "play" function that loops through all value pairs for two indicators selected by widgets.
Screen cap of dashboard
While the loop works, the dashboard resets the axis values for each step of the loop. So what I need is to set axis values based on the widget.value selected. To this end, I have built a data frame "ranges" that has the name of the indicator as index and the min/max value for each indicator as columns.
The updates for controls work thusly (x_axis,etc. are the names of the widgets):
controls = [x_axis, y_axis, start_yr, end_yr, years]
for control in controls:
control.on_change('value', lambda attr, old, new: update())
The update function is supposed to update the ranges upon change in the controls like this:
def update():
p.x_range = Range1d(start = ranges.loc[x_axis.value,"Min"],
end = ranges.loc[x_axis.value,"Max"])
p.y_range = Range1d(start = ranges.loc[y_axis.value,"Min"],
end = ranges.loc[y_axis.value,"Max"])
What should happen: Whenever I change the value of the widget, the ranges should update, but other than that, they should remain constant
What does happen: The ranges are set based on the value of the widget initially set and don't change on update.
I've tried to find examples trying to achieve something similar but no luck.
This is a working example:
import numpy as np
from bokeh.plotting import figure
from bokeh.models import Range1d
from bokeh.io import curdoc
x = np.linspace(0, 100, 1000)
y = np.sin(x)
p = figure(x_range=(0, 100))
p.circle(x, y)
def cb():
# this works:
p.x_range.start += 1
p.x_range.end += 1
# this also works:
#p.x_range = Range1d(p.x_range.start+1, p.x_range.end+1)
curdoc().add_periodic_callback(cb, 200)
curdoc().add_root(p)
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_())
In my project, I have 256 tiny PushButtons in one 16x16 grid layout. (Yeah that took forever.) Editing and running my program now is very laggy. Also, for some strange reason, Qt will not let me enable any of the buttons, but other buttons to the side work just fine?
Is there any easy way to determine which square of the grid was clicked without having a bunch of buttons? (Like following the cursor over an image maybe?)
Also, when each "square" of the grid is clicked, it becomes the "selection" and it needs to be the only "square" selected. (Think about it like a huge chess board)
Here is a pic: http://gyazo.com/988cdbb59b3d1f1873c41bf91b1408fd
Later on, I will need to do this again for a 54x54 size grid (2916 buttons) and I REALLY don't want to do it button by button.
Thanks for your time, I hope you understand my question :)
You can do this easy way, I've explained almost everything in code, but if you have any questions about it feel free to ask, and please, accept this answer if it solved your problem :)
import sys
from PyQt4 import QtGui, QtCore
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class DrawImage(QMainWindow):
def __init__(self, parent=None):
super(QMainWindow, self).__init__(parent)
self.setWindowTitle('Select Window')
#you can set grid size here ... 8x8, 16x16 , for bigger numbers (54x54) be sure your image is big enough, because QWidget can't be smaller then ~20 pixels
self.gridSize = 16
mainWidget = QWidget()
self.setCentralWidget(mainWidget)
self.scene = QGraphicsScene()
view = QGraphicsView(self.scene)
layout = QVBoxLayout()
layout.addWidget(view)
mainWidget.setLayout(layout)
self.image = QImage('image.JPG')# put your image name here, image (suppose to be grid) must be at the same folder or put full path
pixmapItem = QGraphicsPixmapItem(QPixmap(self.image), None, self.scene)
pixmapItem.mousePressEvent = self.pixelSelect
def pixelSelect( self, event ):
#add whatever you want to this widget,any functionality or you can add image for example, I've simply colored it
wdg = QWidget()
layout = QVBoxLayout()
palette = QPalette(wdg.palette())
palette.setBrush(QPalette.Background, QColor(200,255,255))
wdg.setPalette(palette)
wdg.setLayout(layout)
self.scene.addWidget(wdg)
#calculate size and position for added widget
imageSize = self.image.size()
width = imageSize.width()
height = imageSize.height()
#size
wgWidth = float(width)/self.gridSize
wgHeight = float(height)/self.gridSize
wdg.setFixedSize(wgWidth,wgHeight)
#position
wgXpos = int(event.pos().x()/wgWidth) * wgWidth
wgYpos = int(event.pos().y()/wgHeight) * wgHeight
wdg.move(wgXpos, wgYpos)
#which square is clicked?
print "square at row ", int(event.pos().y()/wgHeight)+1,", column ",int(event.pos().x()/wgWidth)+1, "is clicked"
def main():
app = QtGui.QApplication(sys.argv)
form = DrawImage()
form.show()
app.exec_()
if __name__ == '__main__':
main()
Also, if you want to display simple square image over your grid image, look at question/solution I had: QGraphicsPixmapItem won't show over the other QGraphicsPixmapItem
If you don't really need the appearance of buttons, I would create a QWidget subclass that implements a custom paintEvent and renders a grid of needed size (taking the widget size into account). Then, in the mouse events (up,down,move etc.) you can calculate which grid item was clicked with a simple formula. You can render cells with different colors to indicate selection or highlighting.
P.S.:I would really like to post some code from my implementations (i have done this two or three times) but the source codes are at my old company :)
You just create your own QGridLayout in order to be able to add the buttons easily.
I posted an answer to another question, showing you how to fill a custom made QGridLayout with a bunch of widgets sequentially. The buttons are added according to the maximum count of columns you specified. (Note: It's just a very rough example but enough to start from)
In your example you would create the custom grid layout with 16 columns and simply add your buttons.
To find out which button has been pressed (and to make connecting easier) you can use QSignalMapper.
For investigating the lag you could check the amount of (GDI-/User-) handles of your application (using ProcessExplorer for example). The handle count shouldn't be above 10.000.
I don't know why you can't enable the push buttons.