Fluid design in Tkinter - python-3.x

I'm trying to create a somewhat "responsive" design with ttk (tkinter). The basic placement of widgets is no problem at all, but making it fluid with the width of the program is something I cannot achieve. In CSS I know it's possible to say something along the lines of '"float: left" for all containers' and the page would adapt to the screen size. I haven't found something similar to that in Tkinter and frames.
My basic test program:
#!/usr/bin/python3
import tkinter
from tkinter import ttk
from ttkthemes import ThemedTk, THEMES
class quick_ui(ThemedTk):
def __init__(self):
ThemedTk.__init__(self, themebg=True)
self.geometry('{}x{}'.format(900, 150))
self.buttons = {}
self.frame1 = ttk.Frame(self)
self.frame1.pack(side="left")
self.frame2 = ttk.Frame(self)
self.frame2.pack(side="left")
#------------------------------------------------------- BUTTONS
i = 0
while (i < 5):
i += 1
self.buttons[i]= ttk.Button(self.frame1,
text='List 1 All ' + str(i),
command=self.dump)
self.buttons[i].pack(side="left")
while (i < 10):
i += 1
self.buttons[i]= ttk.Button(self.frame2,
text='List 2 All ' + str(i),
command=self.dump)
self.buttons[i].pack(side="left")
def dump(self):
print("dump called")
quick = quick_ui()
quick.mainloop()
This creates a window with 10 buttons all besides each other.
When I shrink the window to the point that the buttons no longer fit on the screen, I would like the buttons to appear below each other
So what I did was add a resize listener and setup the following method:
def resize(self, event):
w=self.winfo_width()
h=self.winfo_height()
# print("width: " + str(w) + ", height: " + str(h))
if(w < 830):
self.frame1.config(side="top")
self.frame2.config(side="top")
But Frame doesn't has the property side, which is a parameter given to the method pack. So that didn't work either.
And now I'm lost. I've spend way to long on this, trying grids and other solutions, but I've got the feeling that I'm missing out on one simple, but very important setting.

I've created a (hackish) solution, which is fine due to it being a very internal program. Since no answer is given in the mean time, I'll provide my solution here. It probably has a lot of room for improvements, but I hope that this can give somebody in the future some pointers on how to tackle this problem by him(or her) self.
#!/usr/bin/python3
import re
import sys
import tkinter
from tkinter import filedialog
from tkinter import ttk
from ttkthemes import ThemedTk, THEMES
import subprocess
import os
from tkinter.constants import UNITS
import json
from functools import partial
class quick_ui(ThemedTk):
def __init__(self):
ThemedTk.__init__(self, themebg=True)
self.minsize(600, 250)
self.elems = {}
self.resize_after_id = None
#------------------------------------------------------- Window menu bar contents
self.menubar = tkinter.Menu(self)
self.menubar.add_command(label="Open", command = self.dump)
self.menubar.add_command(label="Refresh", command = self.dump)
self.config(menu=self.menubar)
# Theme menu
self.themeMenu = tkinter.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label="Theme", menu=self.themeMenu)
self.themeMenu.add_command(label="DEFAULT", command=partial(self.dump, "default"))
#---------------------------------------------------------------------- top_frame
self.top_frame = ttk.Frame(self)
self.top_frame.pack( side = tkinter.TOP, expand='YES', fill='both', padx=10)
self.top_top_frame = ttk.Frame(self.top_frame)
self.top_top_frame.pack(side=tkinter.TOP, expand='YES', fill='both')
self.top_bottom_frame = ttk.Frame(self.top_frame)
self.top_bottom_frame.pack(side=tkinter.BOTTOM)
self.top_bottom_top_frame = ttk.Frame(self.top_frame)
self.top_bottom_top_frame.pack(side=tkinter.TOP)
self.top_bottom_bottom_frame = ttk.Frame(self.top_frame)
self.top_bottom_bottom_frame.pack(side=tkinter.BOTTOM)
#------------------------------------------------------------------- bottom_frame
self.bottom_frame = ttk.Frame(self, relief="sunken")
self.bottom_frame.pack( side = tkinter.BOTTOM,
expand='YES',
fill='both',
padx=10,
pady=10 )
#------------------------------------------------------- BUTTONS
i = 0
while (i < 15):
self.elems[i]=ttk.Button(self.top_bottom_top_frame,
text='List All ' + str(i),
command=self.dump)
i += 1
self.label_test_strings1 = ttk.Label(self.top_top_frame, text='Test strings1')
self.label_test_strings2 = ttk.Label(self.top_bottom_frame, text='Test strings2')
self.label_test_strings4 = ttk.Label(self.top_bottom_bottom_frame, text='Test strings4')
self.label_test_strings1.pack(side = tkinter.TOP)
self.label_test_strings2.pack(side = tkinter.TOP)
self.label_test_strings4.pack(side = tkinter.TOP)
self.placeElems()
# Setup a hook triggered when the configuration (size of window) changes
self.bind('<Configure>', self.resize)
def placeElems(self):
for index in self.elems:
self.elems[index].grid(row=0, column=index, padx=5, pady=5)
# ------------------------------------------------------ Resize event handler
def resize(self, event):
# Set a low "time-out" for resizing, to limit the change of "fighting" for growing and shrinking
if self.resize_after_id is not None:
self.after_cancel(self.resize_after_id)
self.resize_after_id = self.after(200, self.resize_callback)
# ------------------------------------------------------ Callback for the resize event handler
def resize_callback(self):
# The max right position of the program
windowMaxRight = self.winfo_rootx() + self.winfo_width()
# Some basic declarations
found = False
willAdd = False
maxColumn = 0
currIndex = 0
currColumn = 0
currRow = 0
counter = 0
last_rootx = 0
last_maxRight = 0
# Program is still starting up, so ignore this one
if(windowMaxRight < 10):
return
# Loop through all the middle bar elements
for child in self.top_bottom_frame.children.values():
# Calculate the max right position of this element
elemMaxRight = child.winfo_rootx() + child.winfo_width() + 10
# If we already found the first 'changable' child, we need to remove the following child's also
if(found == True):
# Is the window growing?
if(willAdd == True):
# Check to see if we have room for one more object
calcMaxRight = last_maxRight + child.winfo_width() + 20
if(calcMaxRight < windowMaxRight):
maxColumn = counter + 1
# Remove this child from the view, to add it again later
child.grid_forget()
# If this child doesn't fit on the screen anymore
elif(elemMaxRight >= windowMaxRight):
# Remove this child from the view, to add it again later
child.grid_forget()
currIndex = counter
maxColumn = counter
currRow = 1
found = True
else:
# If this child's x position is lower than the last child
# we can asume it's on the next row
if(child.winfo_rootx() < last_rootx):
# Check to see if we have room for one more object on the first row
calcMaxRight = last_maxRight + child.winfo_width() + 20
if(calcMaxRight < windowMaxRight):
child.grid_forget()
currIndex = counter
currColumn = counter
maxColumn = counter + 1
found = True
willAdd = True
# Save some calculation data for the next run
last_rootx = child.winfo_rootx()
last_maxRight = elemMaxRight
counter += 1
# If we removed some elements from the UI
if(found == True):
counter = 0
# Loop through all the middle bar elements (including removed ones)
for child in self.top_bottom_frame.children.values():
# Ignore the elements still in place
if(counter < currIndex):
counter += 1
continue
# If we hit our maxColumn count, move to the next row
if(currColumn == maxColumn):
currColumn = 0
currRow += 1
# Place this element on the UI again
child.grid(row=currRow, column=currColumn, padx=5, pady=5)
currColumn += 1
counter += 1
def dump(self):
print("dump called")
quick = quick_ui()
quick.mainloop()

Related

How do I get the value from an option in tkinter after it has passed through a function?

I'm using tkinter to create an option menu for a user to interact with, when the selection has been made it will pass through a function to return an integer based on the input. I want to be able to return the integer back to a variable outside of the function so it can be received by another file. My problem is that python will keep returning the button as the command has not yet been processed.
from tkinter import *
Final = [] ##List containing options
master = Tk()
variable = StringVar(master)
variable.set(Final[0]) # default value
w = OptionMenu(master, variable, *Final)
w.pack()
def ok():
global choice
choice = variable.get()
global x
x = 0
for i in Final:
if str(i) == str(choice):
break
x += 1
button = Button(master, text="Choose", command=ok)
button.pack()
values = x

program crashes when press button - Number guess game - Tkinter

I have no idea why this is not working, I have been looking and I cant see whats wrong I have messed with it for a while all I want is for it to work.
gets input outputs numbers equal to randomly generated if full number equals the random then you win but it just crashes when I press the button
from tkinter import *
from tkinter import ttk
import random
master = Tk()
master.title('Guess The Number!')
global answer
global guess_entry
global guess_display
answer = str(random.randint(1000,9999))
guess_counter = 0
def callback():
print('Button pressed')
counter = 0
correct = []
while counter < 4:
if guess_entry.get() == answer:
correct.append('Well done, that is correct')
break
elif guess_entry.get()[counter] == answer[counter]:
correct.append(guess_entry.get[counter])
counter += 1
guess_display['text'] = ' '.join(str(correct))
def Help():
win = Toplevel()
win.title('Help')
l = Label(win, text="Guess a 4 digit number and I will tell you\n what you got right,\n keep trying until you get it in the \ncorrect order with correct numbers")
l.grid(row=0, column=0)
b = Button(win, text="Okay", command=win.destroy)
b.grid(row=1, column=0)
guess_entry = Entry(master)
guess_check = Button(master, text='Guess', command=callback)
guess_display = Label(master,text='_ _ _ _')
help_button = ttk.Button(master, text="?", command=Help,width=3)
guess_entry.grid(row=0,column=2)
guess_check.grid(row=1,column=2)
guess_display.grid(row=2,column=1)
help_button.grid(row=0,column=4)
master.mainloop()
If the numbers aren't equal the first time through the loop, they won't be equal any other time through the loop since the user won't have a chance to change their answer while the loop is running.
You can see this by adding a print statement:
while counter < 4:
print("counter:", counter, "guess:", guess_entry.get())
...

Trying to build Risk style game in Tkinter

I have been trying to build my skills in Python and am trying to create a risk style game.
I am not far in to it at the moment as I am trying to get to grips with classes and Tkinter.
My first trial is to create a series of buttons to take the place of the different countries. I then want these buttons to update the amount of armies on the country when they are clicked.
So far I have been able to get the map to generate from the class I have created and the buttons are clickable. When a button is clicked it updates the amount of armies but always for the last button.
How do I get it so that the button I click updates and not the last one?
Have I gone about this in entirely the wrong way?
from tkinter import *
import random
class territory:
def __init__ (self, country, player = "1", current_armies = 0, x=0, y=0):
self.country = country
self.current_armies = current_armies
self.player = player
self.y = y
self.x = x
def get_armies(self):
print(self.country + " has " + str( self.current_armies)+ " armies.")
def add_armies (self, armies):
self.current_armies += armies
def roll_dice (self, dice=1):
rolls = []
for i in range(0, dice):
rolls.append(random.randint(1,6))
rolls.sort()
rolls.reverse()
print (self.country + " has rolled " + str(rolls))
return rolls
def owner(self):
print (self.country + " is owned by " + self.player)
def get_country(self):
print(country)
def button (self):
Button(window, text = territories[0].current_armies, width = 10, command = click1(territories, 0)).grid(row=y,column=x)
window = Tk()
def create_territories():
countries = ["UK", "GER", "SPA", "RUS"]
terr_pos = [[1,0],[2,0],[1,5],[4,1]]
sta_arm = [1,1,1,1]
terr = []
player = "1"
for i in range(len(countries)):
terr.append(territory(countries[i],player, sta_arm [i] , terr_pos[i][0],terr_pos[i][1]))
if player == "1":
player = "2"
else:
player = "1"
return terr
def click1(territory, i):
territory[i].current_armies += 1
build_board(territory)
def build_board(territories):
for i in range(0,4):
Button(window, text = territories[i].country+"\n"+str(territories[i].current_armies), width = 10, command = lambda: click1(territories, i)).grid(row=territories[i].y,column=territories[i].x)
territories = create_territories()
window.title ("Domination")
create_territories()
build_board(territories)
window.mainloop()
In your def button(self):... you are always referencing territories[0]:
Button(window, text=territories[0].current_armies,... command=click1(territories, 0)...
As such, you are always using the first territory as your reference, so you ought to initialize each territory with its index in territories[] so you can pass that into your Button constructor.
On your question of "entirely the wrong way," I'd personally send that question over to CodeReview, since that's more of their domain (we fix broken code, they address smelly code), though there is significant overlap. We do prefer one question per question, however, and "is this whole thing wrong?" is a little broad for StackOverflow.

Creating tkinter gui.How to display information when ID number provided

I am working on creating a gui and could use some help. There are three programs that build off of each other. MY imput will be two CSV files, Examples below:
items4.csv
12345,Italy,100
13579,China,50
ETC.
transactions4.csv
12345,15
13579,10
12345,20
13579,-10
What I want it to do is upon running the gui will open and the window will ask for the "Item ID" Upon entering an id, say 12345, it will open up a new window that displays the following: getID, getName, getAvailableStart, and getAvailableEnd, all of these definitions are were created by the travelclass program (found below) when the program travelToolbox reads the two CSV files.
Note: Both the travelClass and travelToolbox programs are working fine. IT is only travelSystem that needs changing at this point
What I've done:
At this point I have managed to get the travelSystems presents the initial GUI however it doesn't matter what I enter in the entry box because when I hit the open button the next gui opens up but it only shows the information found in the first line of items4.csv. I think my main issue is line 34 or may be line 16. Let me know if any other information is needed.
I could really appreciate some help. Thank you in advance.
travelsystem
from travelToolbox import readItems, readTransactions
from tkinter import *
from tkinter import font
import tkinter as tk
class myApp :
def __init__(self, top, itemRecords) :
self.root = top
# Create a container Frame at the bottom
self.bframe = Frame(self.root)
self.bframe.pack(side=BOTTOM) # Create Label
self.xlabel = Label(self.root, text="Item ID")
self.xlabel.pack(side=LEFT)
self.xentry = Entry(self.root, bd=5) # Create Entry box
self.xentry.pack(side=LEFT)
self.xentry.focus_set() # Set focus in Entry box
self.xopen = Button(self.root, text="Open", command=self.showStockItem) # Create open Button
self.xopen.pack(side=LEFT)
self.xquit = Button(self.bframe, text="Quit", command=self.quitit ) # Create quit Button
#self.root instead of bframe
self.xquit.pack(side=BOTTOM)
def showStockItem(self):
tbl = Toplevel() # Create Toplevel window
hdr = font.Font(underline=1, weight="bold") # Create header font
tid = Label(tbl, text="ID", font=hdr) # Create 4 column headers in grid
tid.grid(row=0, column=0)
tlast = Label(tbl, text="Name", font=hdr)
tlast.grid(row=1, column=0)
tfirst = Label(tbl, text="Start", font=hdr)
tfirst.grid(row=2, column=0)
tlabel = Label(tbl, text="End", font=hdr)
tlabel.grid(row=3, column=0)
for rec in itemRecords.values() :
tid = Label(tbl, text= rec.getID()) # Create 4 column headers in grid
tid.grid(row=0, column=1)
tlast = Label(tbl, text= rec.getName())
tlast.grid(row=1, column=1)
tfirst = Label(tbl, text= rec.getAvailableStart())
tfirst.grid(row=2, column=1)
tlabel = Label(tbl, text= rec.getAvailableEnd())
tlabel.grid(row=3, column=1)
#self.xquit.pack(side=BOTTOM) #May not need this.
return
def quitit(self):
self.root.destroy()
return
itemsFileName = "items4.csv"
transactionsFileName = "transactions4.csv"
# itemRecords is a dictionary of stockItem records indexed by item ID
itemRecords = {}
# Read the items from itemsFileName into itemRecords
readItems(itemsFileName, itemRecords)
# Read the transactions from transactionsFileName into itemRecords
readTransactions(transactionsFileName, itemRecords)
top = tk.Tk()
#top = Travel()
app = myApp(top, itemRecords)
top.mainloop()
Other files:
travelToolbox
import csv
from travelClass import travelItem
def readItems(itemsFileName, itemRecords) :
# readItems reads items from itemsFileName into the itemRecords dictionary
# Open itemsFileName and create a CSV file reader
itemsFile = open(itemsFileName, 'r')
itemsReader = csv.reader(itemsFile)
# Process each row of the items file
for row in itemsReader :
# Get the values for the record
(iid, iname, icount) = row
iid = str(iid)
iname = str(iname)
icount = int(icount)
# Check if this ID is not yet in itemRecords
if (not(iid in itemRecords)) :
# Create a travelItem object and add it to itemRecords
itemRecord = travelItem(iid, iname, icount)
itemRecords[iid] = itemRecord
def readTransactions(transactionsFileName, itemRecords) :
# readTransactions reads transactions from transactionsFileName into the itemRecords dictionary
transactionFile = open(transactionsFileName, 'r')
transactionReader = csv.reader(transactionFile)
for row in transactionReader :
# Get the values for the record
(iid, itransaction) = row
iid = str(iid)
itransaction = int(itransaction)
itemRecords[iid].appendTransaction(itransaction)
#for key, value in itemRecords.items() :
transactionFile.close()
travelClass
class travelItem :
Class travelItem :
def __init__(self, itemID, itemName, itemCount) :
# Constructor to create inventoryItem CORRECT
self.id = itemID
self.name = itemName
self.AvailableStart = itemCount
self.transactions = []
def getID(self) :
# getID returns the tour ID CORRECT
return(self.id)
def getName(self) :
# getName returns the tour name CORRECT
return (self.name)
def setName(self, newName) :
# setName sets the tour name
self.name = newName
def getAvailableStart(self) :
# returns the starting availability
return (self.AvailableStart)
def appendTransaction(self, num) :
# appendTransaction appends a transaction to the transactions list
self.transactions.append(num)
def getTransactions(self) :
# getTransactions returns the list of transactions
return (self.transactions)
def getReservations(self) :
# returns the total of reservation transactions
total = 0
for num in self.transactions :
if ((num) > 0):
total += (num)
self.Reservations = total
return(self.Reservations)
def getCancellations(self) :
# returns the total of cancellation transactions
total = 0
for num in self.transactions :
if (num <= 0):
total += (num)
return(total)
def getAvailableEnd(self) :
# returns the ending availability, which is availableStart minus transactions
self.AvailableEnd = ((self.AvailableStart) - (self.Reservations) - (self.getCancellations()))
return(self.AvailableEnd)

gtk progressbar doesn't work anymore after matplot event?

Hi I'm new at programming in python and gtk.
I'm writing a program to do some measurement.
For plotting the measurement, I use matplotlib.
The program will have a function to turn a heater on and off and to make the measurement.
I want to use separate threads for the heater and the measurement.
For now the communication with the hardware hasn't been implemented yet in this program.
The problem is when I click the "measurebutton", the "progressbar" doesn't work anymore.
I get a message:
gtk.ProgressBar object at 0x29b8460 (uninitialized at 0x0)
When I only use the heaterbutton, the progressbar keeps working
What am I doing wrong ?
This is the code
#!/usr/bin/env python
import pygtk
pygtk.require('2.0')
import gtk
import time
import gobject
import threading
import matplotlib
import numpy as np
from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas
class measure:
# callback to quit
def delete_event(self, widget, event, data = None):
gtk.main_quit()
return False
def heater_helper(self, widget, heater, progressbar):
print "starting heater thread"
threading.Thread(target=self.heater_cb, args=(widget, heater, progressbar)).start()
def heater_cb(self, widget, heater, progressbar):
heaterstring = "6.3"
heater = eval(heaterstring)
stap = 1
j = 0.1
heatervalue = widget.get_active()
print heatervalue
progressbar.set_fraction(0.1)
while (stap <= 10 ):
if widget.get_active():
print widget.get_active()
fraction = j * stap
print fraction
progressbar.set_fraction(fraction)
stap = stap + 1
time.sleep(1)
else:
stap = 11
progressbar.set_fraction(0.0)
break
def do_measurement_helper(self, widget, fig):
print " Start measurement thread"
threading.Thread(target=self.do_measurement, args=(widget, fig)).start()
def do_measurement(self, widget, fig):
fig.clear()
ax = fig.add_subplot(111)
x = np.arange(0, 5*np.pi, 0.01)
y = np.sin(x**2)*np.exp(-x)
ax.plot(x, y)
fig.canvas.draw()
def __init__(self):
# Create new Window
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.window.connect("delete_event", self.delete_event)
self.window.show()
mainbox = gtk.HBox(False, 0)
self.window.add(mainbox)
mainbox.show()
leftvbox = gtk.VBox(False, spacing = 10)
mainbox.pack_start(leftvbox, expand = False, fill = False, padding = 0)
leftvbox.show()
rightvbox = gtk.VBox(False, spacing = 10)
mainbox.pack_start(rightvbox, expand = False, fill = False, padding =0)
rightvbox.show()
heaterprogressbar = gtk.ProgressBar()
leftvbox.pack_start(heaterprogressbar, expand = False, fill = False, padding = 0)
heaterprogressbar.show()
heaterbutton = gtk.ToggleButton("Heater")
leftvbox.pack_start(heaterbutton, expand = True, fill = False, padding = 0)
heaterbutton.show()
heaterbutton.connect("toggled", self.heater_helper, heaterbutton, heaterprogressbar)
fig = matplotlib.figure.Figure(figsize=(5,4), dpi=64)
canvas = FigureCanvas(fig)
rightvbox.pack_start(canvas, expand = True, fill = True, padding = 0 )
canvas.show()
measurebutton = gtk.Button("Measure")
rightvbox.pack_start(measurebutton, expand = False, fill = False, padding = 0)
measurebutton.show()
measurebutton.connect("clicked", self.do_measurement_helper, fig)
def main():
gtk.main()
return(0)
if __name__ == "__main__":
gtk.gdk.threads_init()
measure()
main()
gtk.gdk.threads_leave()
Kind regards,
Joris Weijters
Combining threads, Matplotlib, and the GTK main loop is probably not supported and difficult to debug exactly what is going on. My advice is not to do any GUI calls from threads, but instead schedule them using gobject.idle_add().
Threads, Matplotlib and the GTK main loop can be combined, if you keep in mind some things:
I use gobject.threads_init() instead of gtk.gdk.threads_init(), the gtk.gdk variant did not work for me in combination with Matplotlib. I think you can also omit the gtk.gdk.threads_leave().
As ptomato mentioned, you should let the main gtk thread handle anything that has to do with gtk widgets by calling the gobject.idle_add() and gobject.timeout_add() functions.
I usually make a helper function to periodically update the statusbar from a float variable:
def do_measurement(self):
self.data = []
self.progress = 0
self.abort = threading.Event()
gobject.timeout_add(100, self.update_progressbar)
for point in some_generator_yielding_100_values():
if self.abort.is_set():
break
self.data.append(point)
self.progress += 0.01
self.progress = None
def update_progressbar(self):
if self.progress is None:
self.progressbar.set_fraction(0) # reset bar
return False # do not run again
self.progressbar.set_fraction(self.progress)
return True # run again after 100ms
def start_measurement(self):
threading.Thread(target=self.do_measurement).start()
def stop_measurement(self):
self.abort.set()
But you can of course also just call gobject.idle_add(self.progressbar.set_fraction, x) to set the new value x asynchroneously.

Resources