What I am trying to do is:
Create a main frame (pc_gui) and two sub frames (video_frame) and (side_frame) respectively.
Allow the user to be able to resize the application window to suit and have all frames, widgets, images, etc. resize accordingly.
Reference frames elsewhere in the gui.
In attempt to keep things tidy, possibly in error, I have an Application class, as well as functions: init, app_size, create_frames, create_widgets, and an updater function. There is quite a bit more, but if I can get this core part to work right I should be able to keep making progress.
This begins with the relatively standard tkinter UI initialization which includes the starting size of the application window.
# Initialize Application Window
pc_gui = tk.Tk()
pc_gui.geometry("1280x720")
# Initialize Updater Variables
UPDATE_RATE = 1000
# Run application
app = Application(pc_gui)
pc_gui.mainloop()
Then I create the class, main frame (pc_gui), and I call the various sub frame, application size, and images functions.
class Application(tk.Frame):
""" GUI """
def __init__(self, pc_gui):
""" Initialize the frame """
tk.Frame.__init__(self, pc_gui)
self.app_size()
self.grid()
self.create_frames()
self.create_images()
self.updater()
This is the app_size function. It's purpose is to parse the current size of the application window and the display monitor assuming the user will change these to suit while the program is running.
def app_size(self):
pc_gui.update_idletasks() # Get Current Application Values
app_width = pc_gui.winfo_width() # Current Application Width
app_height = pc_gui.winfo_height() # Current Application Height
disp_width = pc_gui.winfo_screenwidth() # Monitor (Screen) Width
disp_height = pc_gui.winfo_screenheight() # Monitor (Screen) Height
return app_width, app_height, disp_width, disp_height
Then I create the sub frames using the app_size values. The colors are simply there to help me keep track of things during development.
def create_frames(self):
""" Create Frames """
app_size = self.app_size() # Get size wxh of application window and display
app_width = app_size[0]
app_height = app_size[1]
disp_width = app_size[2]
disp_height = app_size[3]
geometry = "%dx%d" % (app_width, app_height) # Create text value for geometry
pc_gui.geometry(geometry) # Set Application Window Size
# Section of Application window dedicated to source video
video_frame = tk.Frame(
master = pc_gui,
width = app_width*.75,
height = app_height,
bg = "blue"
)
video_frame.place(
x = 0,
y = 0
)
# Section of Application window dedicated to calculations
side_frame = tk.Frame(
master = pc_gui,
width = app_width*.25,
height = app_height,
bg = "red"
)
side_frame.place(
x = app_width*.75,
y = 0
)
pc_gui.update_idletasks()
sf_x = side_frame.winfo_x()
sf_y = side_frame.winfo_y()
sf_w = side_frame.winfo_width()
sf_h = side_frame.winfo_height()
return sf_x, sf_y, sf_w, sf_h
def updater(self):
#self.create_frames() # Update Application Window
self.create_images() # Update Images Widget
self.after(UPDATE_RATE, self.updater) # Call "updater" function after 1 second
Then I start creating widgets. For the image (Label) widgets I would like to use grid, but I am having the hardest time figuring out how to reference the sub frame (side_frame) in the create_images function. I have truncated the important bits to make this question more to the point.
def create_images(self):
sf_dims = self.create_frames()
sf_x = sf_dims[0]
sf_y = sf_dims[1]
sf_w = sf_dims[2]
sf_h = sf_dims[3]
...
fp1_lbl = tk.Label(
master = pc_gui,
image = fp1
)
fp1_lbl.image = fp1
fp1_lbl.place(
x=sf_x + img_padding,
y=sf_y + 20,
anchor='nw'
)
Everything up to this point "works" admittedly rather inefficiently. What I would like to do is not have those sf_x, _y, _w, and _h hanging out there in the wind and it follows that I would also not have to call the frame function from widget function in order to get them.
The reason is that I feel it's not in the right spirit of python frames as I would like to use Grid on the side frame only but create (or use) Grid from the widget function (the point of creating the side_frame in the first place) and I would prefer to only refresh the sub parts of the application window that need to be refreshed, and not the whole smash every 1 second.
What ends up happening is the images flicker every 1 second, even when nothing needs to be updated and while the images do resize according to the application window I am doing this with the Place functionality and effectively ignoring the existence of side_frame.
My current attempts have been around the following
fp1_lbl = tk.Label(
master = self.side_frame,
image = fp1
)
fp1_lbl.image = fp1
fp1_lbl.place(
x=sf_x + img_padding,
y=sf_y + 20,
anchor='nw'
)
I get versions of the following error:
AttributeError: 'Application' object has no attribute 'side_frame'
You need to name things with the "self" prefix in order to use them in other methods. So when you create the subframes:
# Section of Application window dedicated to calculations
self.side_frame = tk.Frame(
master = pc_gui,
width = app_width*.25,
height = app_height,
bg = "red"
)
self.side_frame.place(
x = app_width*.75,
y = 0
)
Now you can use them anywhere in your class as you have tried:
fp1_lbl = tk.Label(
master = self.side_frame,
image = fp1
)
Remember there is no implied relationship between a variable named foo and self.foo. They are 2 completely unrelated names.
As for your resizing, tkinter will do that for you. You can use the relwidth and relheight arguments to set the width and height to a fraction between 0 and 1. Here's a demo:
import tkinter as tk
class Application(tk.Frame):
""" GUI """
def __init__(self, pc_gui):
""" Initialize the frame """
tk.Frame.__init__(self, pc_gui)
self.create_frames()
def create_frames(self):
# insert the subframes in *this* Frame (self), not in the master Frame
self.video_frame = tk.Frame(self, bg = "blue")
self.video_frame.place(relheight=1.0, relwidth=.25)
self.side_frame = tk.Frame(self, bg = "red")
self.side_frame.place(relheight=1.0, relwidth=.75, relx=1.0, anchor='ne')
pc_gui = tk.Tk()
pc_gui.geometry("1280x720")
win = Application(pc_gui)
win.pack(fill=tk.BOTH, expand=True)
tk.Button(pc_gui, text = "Don't click me!").pack()
pc_gui.mainloop()
Related
what's the best way to embed multiple PyMEL layouts in a single form?
For example, for each row of a form, I want to specify that:
row one uses a 2-column layout for label+textField pair
row two uses a 1-column layout for a menu item that has its own annotation label
row three adds a full-width execution button to the 1-column layout
all controls scale appropriately if the window is resized by a user
TIA!
import pymel.core as pm
def test(*args):
print('model name: {}'.format(modelTField.getText()))
print('model geom: {}'.format(modelMenu.getValue()))
if pm.window("testUI", ex=1): pm.deleteUI("testUI")
window = pm.window("testUI", t="Test v0.1", w=500, h=200)
mainLayout = pm.verticalLayout()
# two column layout
col2Layout = pm.horizontalLayout(ratios=[1, 2], spacing=10)
pm.text(label='Model name')
global modelTField
modelTField = pm.textField()
col2Layout.redistribute()
# single column layout
col1Layout = pm.horizontalLayout()
global modelMenu
modelMenuName = "modelMenu"
modelMenuLabel = "Model mesh"
modelMenuAnnotation = "Select which geo corresponds to the model shape"
modelMenu = pm.optionMenu(modelMenuName, l=modelMenuLabel, h=20, ann=modelMenuAnnotation)
pm.menuItem(l="FooShape")
pm.menuItem(l="BarShape")
# execute
buttonLabel = "[DOIT]"
button = pm.button(l=buttonLabel, c=test)
col2Layout.redistribute()
# display window
pm.showWindow(window)
Since the pm.horizontalLayout() and pm.verticalLayout automatically redistribute the content, you do not need to call the redistribute yourself. But for these functions to work you need a with statement like this:
import pymel.core as pm
def test(*args):
print('model name: {}'.format(modelTField.getText()))
print('model geom: {}'.format(modelMenu.getValue()))
if pm.window("testUI", ex=1): pm.deleteUI("testUI")
window = pm.window("testUI", t="Test v0.1", w=500, h=200)
with pm.verticalLayout() as mainLayout:
with pm.horizontalLayout(ratios=[1, 2], spacing=10) as col2Layout:
pm.text(label='Model name'
global modelTField
modelTField = pm.textField()
with pm.horizontalLayout() as col1Layout:
global modelMenu
modelMenuName = "modelMenu"
modelMenuLabel = "Model mesh"
modelMenuAnnotation = "Select which geo corresponds to the model shape"
modelMenu = pm.optionMenu(modelMenuName, l=modelMenuLabel, h=20, ann=modelMenuAnnotation)
pm.menuItem(l="FooShape")
pm.menuItem(l="BarShape")
# execute
buttonLabel = "[DOIT]"
button = pm.button(l=buttonLabel, c=test)
# display window
pm.showWindow(window)
To create UI with python it helps a lot if you enclose everything into a class, especially if you use PyMel. This way you could do:
class MyWindow(pm.ui.Window):
def __init(self):
self.title = "TestUI"
def buttonCallback(self, *args):
do something....
This is very helpful if you have callbacks since everything you need can be accessed from within the class and you need no global variables at all.
I am a complete beginner in python,I was hoping someone could help me figure out what I am trying to accomplish.
I built a small tkinter front end that will generate a string multiple times. I want the amount of times that the string is generated to be based off of an entry box. I have hit a wall.
Here is my Front End (help_FE.py)
from tkinter import *
import help_BE
def view_formula():
text1.delete('1.0',END)
text1.insert(END,help_BE.End_result)
pass
window = Tk()
window.wm_title=("Print:")
l1=Label(window,text = "Page to print:")
l1.grid(row=3, column=2)
e1 = Entry(window)
e1.grid(row=3, column=3)
text1=Text(window, height=20,width=35)
text1.grid(row=4,column=3, rowspan=10, columnspan=7, padx=5, pady=10)
sb1=Scrollbar(window)
sb1.grid(row=4,column=11,rowspan=10)
text1.configure(yscrollcommand=sb1.set)
sb1.configure(command=text1.yview)
text1.bind('<<TextboxSelect>>')
b1=Button(window, text = "Generate", width =10, command=view_formula)
b1.grid(row=3,column=6)
window.mainloop()
and my backend (help_BE.py)
currently, the generate button will print "Testing" 3 times, because i have set pages = 3 in the backend, but I want to be able to set pages to whatever is entered into the frontend entry box.
pages = 3
result=[]
def foo():
skip_zero = pages + 1
for x in range (skip_zero):
if x==0:
continue
result.append("Testing"+str(x))
listToStr = ''.join([str(element) for element in result])
full_formula = (listToStr)
return full_formula
End_result = foo()
The data inputted in the field can be accessed using the function Entry.get(), and you can convert the string to a number with the int function.
In order to get it to the backend, and in order to keep your value up to date, I would make sure that, as jasonharper mentioned, you call foo each time the button is pressed, passing the entry's value in as the argument pages. This means tweaking your code as such:
help_BE.py
def foo(pages):
skip_zero = pages + 1
for x in range (skip_zero):
if x==0:
continue
result.append("Testing"+str(x))
listToStr = ''.join([str(element) for element in result])
full_formula = (listToStr)
return full_formula
help_FE.py
def view_formula():
text1.delete('1.0',END)
text1.insert(END,help_BE.foo(int(e1.get())))
SO I am using Python 3.4 and tkinter.
And when I call a function again n again which contains a label, the label keeps on appearing in window but previous label doesn't go away?
How can I remove any printed label from GUI window as soon as function is called and then display new one?
Here is the code:-
#def prestart():
#here I check if number of match is okay, if not, user is redirected to setting else, I call start()
def start():
#CPU Choice
cpu_choice = Label(historyframe, text = "CPU Choosed: {}".format(dict['cpu_choice']))
#Played Match
#played_num_of_match = Label(scoreframe, text = "Number of Matches Played: {}".format(int(dict['match_played'])))
#Display Status
status_disp = Label(scoreframe, text = "Current Status: {}".format(dict['status']))
if(int(dict['match_played']) < int(dict['num_of_match'])):
playframe.grid(row = 1, column = 0)
historyframe.grid(row = 2, column = 1)
status_disp.pack(fill=X)
elif(int(dict['match_played']) == int(dict['num_of_match'])):
playframe.grid(row = 1, column = 0)
historyframe.grid(row = 2, column = 1)
status_disp.pack(fill=X)
cp = dict['cpu_point']
up = dict['user_point']
result(cp, up)
cpu_choice.pack(fill = X)
scoreframe.grid(row = 2, column = 0)
This function just updates the display!
def send_value(x):
#Here I run logic of game and change value of key in dictionary and call start() at end of change.
Now, the choice buttons are not in any definition as they don't need to be called again n again. I just make playframe disappear n appear!
Here is the code for them:-
#Display Question
question = Label(playframe, text = "Rock? Paper? Scissor?")
#Rock
rock = Button(playframe, text = "Rock!", command = lambda: send_value("ROCK"))
#Paper
paper = Button(playframe, text = "Paper!", command = lambda: send_value("PAPER"))
#Scissor
scissor = Button(playframe, text = "Scissor!", command = lambda: send_value("SCISSOR"))
So when user clicks Rock/Paper/Scissor, I just change key value in dictionary! But if I keep the label outside function, it doesn't get auto updated!
Everything else is working perfectly. I'll kind of now start to make code cleaner.
Try something like this instead of creating a new label every time:
import Tkinter as tk
class Window():
def __init__(self, root):
self.frame = tk.Frame(root)
self.frame.pack()
self.i = 0
self.labelVar = tk.StringVar()
self.labelVar.set("This is the first text: %d" %self.i)
self.label = tk.Label(self.frame, text = self.labelVar.get(), textvariable = self.labelVar)
self.label.pack(side = tk.LEFT)
self.button = tk.Button(self.frame, text = "Update", command = self.updateLabel)
self.button.pack(side = tk.RIGHT)
def updateLabel(self):
self.i += 1
self.labelVar.set("This is new text: %d" %self.i)
root = tk.Tk()
window = Window(root)
root.mainloop()
Important points:
1) A class is used, as it is much easier to pass values around when all Tkinter objects and variables are member variables, accessible from all of your GUI functions.
2) updateLabel does not create a new Label. It simply updates the StringVar() object to hold new text every time you call the function. This is accomplished with the textvariable = self.labelVar keyword when creating my Label widget.
PS: This is done in Python 2.5 so for this code to work for you, change Tkinter to tkinter
EDIT 06/19/2015:
If you want to implement something similar to what I have with your code, without using a class, you'll need to pass around references to your variables.
1) Change start:
Your Labels cpu_choice, status_disp, etc. should be created outside of the function; likely in the same location as question, rock, paper, scissors, etc. You will also pack them outside of the function as well. Same with all the calls to .grid inside of start; you shouldn't need to call pack or grid more than once: right when you create the widget.
The following lines:
playframe.grid(row = 1, column = 0)
historyframe.grid(row = 2, column = 1)
status_disp.pack(fill=X)
Can be done outside of the function as well; you execute these 3 statements under both the if and the elif conditions. This means they aren't really conditional statements; they are done regardless of the validity of the condition.
2) Create a StringVar for both cpu_choice & status_disp & edit the Labels as follows (remember, outside of the function):
cpu_choice_text = StringVar()
cpu_choice_text.set("Set this to whatever is shown at the start of the game")
cpu_choice = Label(historyframe, text = cpu_choice_text.get(), textvariable = cpu_choice_text)
cpu_choice.pack(fill = X)
# And do this same thing for status_disp
3) When you call start, you will now pass it cpu_choice_text & status_disp_text (or whatever they are called). Instead of trying to change the text field of the Label frame, you may now use a set call on the StringVar which is connected to the Label & the Label will automatically update. Example:
def start(cpu_choice_text, status_disp_text):
cpu_choice.set(text = "CPU Choice: {}".format(dict['cpu_choice']))
...
Alternatively, wrap it all in a class and make it much easier for yourself by using self on every Tkinter variable & widget. In this way you won't need to pass variables to your functions, just access member variables directly as I have with self.i, self.labelVar in my example.
Each time you call start you create new labels and use grid to place them in the same spot as the old labels. The best solution is to only create the labels once, but if you insist on creating new labels each time start is called, you need to delete the old labels first.
You can use the destroy() method of a label to destroy it, though for that to work you must keep a global reference of the label.
So, I've got a list with entries that look like this:
Option1 Placeholder1 2 Placeholder2 0
Option2 Placeholder1 4
Option3 Placeholder1 2 Placeholder2 -2 Placeholder3 6
I have a listbox of the Options and a button that creates a new window with the values for the selected Option. What I want to do is to create n number of buttons when this new window is created, where n is the number of values of the selected Options (i.e. 2, 1 and 3 for Options 1 through 3, respectively). I want it to look something like this:
Option1
Placeholder1 [button1 containing value=2]
Placeholder2 [button2 containing value=0]
... which is of course quite simple if I just assign a button for the maximum number of n that I know will be present, but I'm wondering if there's a way to do it more arbitrarily. Obviously the same problem applies to the arbitrary number of Labels I would need to use for the value names (the 'PlaceholderX's) as well.
I've been trying to do some reading on this type of thing, variable variables, etc., and it seems it's a very big NO-NO most (if not all) of the time. Some advocate the use of dictionaries, but I don't really get how that's supposed to work (i.e. naming variables from entries/values in a dict).
Is this something that can (and should) be done, or am I better off just creating all the buttons manually?
[EDIT: added code]
from tkinter import *
import csv
root = Tk()
root.wm_title("RP")
listFrame = Frame(root, bd=5)
listFrame.grid(row=1, column=2)
listbox1 = Listbox(listFrame)
listbox1.insert(1, "Option1")
listbox1.insert(2, "Option2")
listbox1.insert(3, "Option3")
listbox1.pack()
infoFrame = Frame(root, bd=5)
infoFrame.grid(row=1, column=3)
info_message = Message(infoFrame, width=300)
info_message.pack()
# Read stats from file
stat_file = open('DiceTest.csv', 'rU')
all_stats = list(csv.reader(stat_file, delimiter=';'))
def list_selection(event):
# gets selection and info/stats for info_message
index = int(listbox1.curselection()[0])
stats = all_stats[index]
infotext = str(stats[0]) # just the name
for n in range(int((len(stats)-2)/2)): # rest of the stats
infotext += ('\n' + str(stats[n*2 + 2]) + '\t' + str(stats[n*2 + 3]))
info_message.config(text=infotext)
listbox1.bind('<ButtonRelease-1>', list_selection)
def load():
top = Toplevel()
top.geometry('300x100')
index = int(listbox1.curselection()[0])
stats = all_stats[index]
# some way to create arbitrary buttons/labels here (?)
load_button = Button(root, text='Load', command=load)
load_button.grid(row=2, column=2)
root.mainloop()
Oh, and every button should have the same command/function, which reduces whatever value currently is in the button by 2.
Figured it out! Creating the widgets dynamically with a dictionary worked just fine, but calling the correct widget on the various button presses was more difficult. This is what I had:
buttons = dict()
for k in range(len(info)):
buttons[k] = Button(top, text=info[k], command=lambda: my_function(buttons[k]))
... which would work, but all button presses would call the function with the last created button as the target. All that was needed was a few extra characters in the command part of the buttons:
buttons = dict()
for k in range(len(info)):
buttons[k] = Button(top, text=info[k], command=lambda a=k: my_function(buttons[a]))
... which I assume works because it somehow stores the value of k inside a rather than taking the last known value of k, i.e. equivalent to the last created button. Is this correct?
You can store Buttons in a list:
from tkinter import *
master = Tk()
buttons = []
n = 10
for i in range(n):
button = Button(master, text = str(i))
button.pack()
buttons.append(button)
master.mainloop()
I have a QScrollArea with a QWidget (content_widget) inside in my application. I want to fill it with images while the application is running. I would like to have the scroll behavior if the number of images exceeds the number which can be shown without scrolling. I came this far:
children_occupied_width = self.ui.content_widget.childrenRect().width()
image_view = QtGui.QGraphicsView(self.ui.content_widget)
image_view.setGeometry(QtCore.QRect( children_occupied_width, 0, 210, 210 ))
image_view.setObjectName("New Image")
placeholder_image = QtGui.QGraphicsScene()
placeholder_image.addPixmap(QtGui.QPixmap('placeholder.png'))
image_view.setScene(placeholder_image)
image_view.show()
Although the images appear in the list at the right position there is no scrolling if the images start to be placed outside of the visible area. The size of content_widget seems not to change even with
self.ui.content_widget.resize(...)
or
self.ui.content_widget.adjustSize()
How to make it grow/resize?
This first part turned out to not be what you really needed. I am leaving it for informational purposes, but see the update at the bottom
The problem is that the QGraphicsView is itself a type of scroll widget, representing a slice of the scene it is viewing. Ideally you would just use the view by itself and make use of its own scrollbars.
But if you have specific need to forward the content size of the view up to a normal QWidget scroll, then what you would need to do is make your QGraphicsView always resize itself when the contents of the scene change. The QScrollArea is only going to respond to size changes of the widget it is set to. The view needs to change size. The process would be that the view needs to listen to signals from the scene for added or removed items, and then resize itself to completely enclose all of those children.
Here is an example of how the QGraphicsView widget, on its own, is perfectly capable of serving as the scroll functionality:
from PyQt4 import QtCore, QtGui
app = QtGui.QApplication([])
scene = QtGui.QGraphicsScene()
view = QtGui.QGraphicsView()
view.setScene(scene)
view.resize(600,300)
pix = QtGui.QPixmap("image.png")
w,h = pix.width(), pix.height()
x = y = 0
pad = 5
col = 3
for i in xrange(1,20):
item = scene.addPixmap(pix)
item.setPos(x,y)
if i % col == 0:
x = 0
y += h+pad
else:
x+=w+pad
view.show()
view.raise_()
app.exec_()
You can see that when the images overflow the current size of the view, you get scrollbars.
If you really need to have some parent scroll area acting as the scroll for the view (for reasons I do not really understand), then here is a more complex example showing how you would need to watch for some event on the scene and then constantly update the size of the view to force scrollbars on the parent scrollarea. I have chosen to watch a signal on the scene for when its rect changes (more children are added)
from PyQt4 import QtCore, QtGui
app = QtGui.QApplication([])
win = QtGui.QDialog()
win.resize(600,300)
layout = QtGui.QVBoxLayout(win)
scroll = QtGui.QScrollArea()
scroll.setWidgetResizable(True)
layout.addWidget(scroll)
view = QtGui.QGraphicsView(scroll)
view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
scene = QtGui.QGraphicsScene(scroll)
view.setScene(scene)
scroll.setWidget(view)
pix = QtGui.QPixmap("image.png")
w,h = pix.width(), pix.height()
x = y = i = 0
pad = 5
col = 3
def createImage():
global x,y,i
item = scene.addPixmap(pix)
item.setPos(x,y)
i+=1
if i % col == 0:
x = 0
y += h+pad
else:
x+=w+pad
def changed():
size = scene.itemsBoundingRect().size()
view.setMinimumSize(size.width()+pad, size.height()+pad)
scene.changed.connect(changed)
t = QtCore.QTimer(win)
t.timeout.connect(createImage)
t.start(500)
win.show()
win.raise_()
app.exec_()
The scroll area is always looking at the size of the widget that has set as the child. The view must be resized.
Update
As it turns out from your comments, you didn't really need to use the QGraphics objects for this approach. It only made your task more complicated. Simply use a vertical layout and add QLabel widgets to it:
from PyQt4 import QtCore, QtGui
app = QtGui.QApplication([])
win = QtGui.QDialog()
win.resize(300,300)
layout = QtGui.QVBoxLayout(win)
scroll = QtGui.QScrollArea()
scroll.setWidgetResizable(True)
layout.addWidget(scroll)
scrollContents = QtGui.QWidget()
layout = QtGui.QVBoxLayout(scrollContents)
scroll.setWidget(scrollContents)
pix = QtGui.QPixmap("image.png")
def createImage():
label = QtGui.QLabel()
label.setPixmap(pix)
layout.addWidget(label)
t = QtCore.QTimer(win)
t.timeout.connect(createImage)
t.start(500)
win.show()
win.raise_()
app.exec_()
I found at least a reason why resize did not worked in my code. Setting widgetResizable property of QScroll area to false (or disabling it in the Qt Designer) made it work.