app structuring / re importing modules with tkinter python3 - python-3.x

I am restructuring this question: I felt the original questions were long winded and not particularly helpful to anyone else.
I have a main app (run as__main__) that runs directly.
I have a module (simple_module.py) that I only wish to use as an import.
I realise that I can run it standalone if required (via if__name__) and have including that in the modules code, for demonstration only.
When user presses main.py's 'start' button, it should open a new toplevel window with all the classes and widgets from the simple_module which are all in a single class called Page. (all whilst the main app window remains open.)
I want the module to be re-imported (or equivalent) every time the button is pressed. When the modules 'close' button or X is pressed I want it to close that window. main.py's window remains open throughout this and the button press needs to re-open that module window, infinitely, like a loop.
I have added if name == 'main' just to highlight that I understand what this does (it is normally within all my main.py apps) and why I still cannot get the result I want. As far as I can see, it does not change anything, I am now only importing the class but 'new' is still not recognised. Which is the same issue as in the prior example.
I have main.py
import tkinter as tk
# audio module works as expected
import audio_module as am
# I want this window to open and close on command
import simple_module as sm
class GUI(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
#self.new = tk.Toplevel(self) # auto loads a second, unwanted window
self.session_counter = 0
self.start_btn = tk.Button(root, text="start", command=self.start)
self.start_btn.grid(row=4,column=0,sticky="nsew",pady=30, padx=30, ipady=18)
def start(self):
am.spell() # these audio imports work like a charm, every btn press - single functions call OK
self.session_counter += 1
print(self.session_counter)
#import simple_module - if used here, my usual 'illegal' import style (works great, once only,
# unless in same script as __main__ in which case all re-imports work fine)
# Import attempts
#import simple_module as sm
#page = Page(new) # Page not defined
#sm.Page() #missing parent arg (new)
# error: 'new' not defined
#sm.Page(new)
if __name__ == '__main__':
print('running as __main__')
root = tk.Tk()
#sm.Page = tk.Toplevel(new) # a desperate attempt NO
#page = sm.Page(tk.TopLevel) NO
# qualify and USE module here! sm is not required if you use 'from simple_module import Page' !!
page = sm.Page(root)
#page.pack(fill='both', expand=True)
page.grid(row=0,column=0,sticky='nsew')
main = GUI(root)
root.mainloop()
Finally, we have simple_module.py:
import tkinter as tk
import audio_module as am
# this module works exactly as expected IF run directly...
class Page(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
# super().__init__(*args, **kwargs)
self.back_btn = tk.Button(parent, text="close", command=self.back)
self.back_btn.grid(row=4,column=0,sticky="nsew",pady=30, padx=30, ipady=18)
def back(self):
am.click()
# close this page BUT have it ready to re-open IF user re-presses button.
new.destroy()
if __name__ == "__main__":
print('running as __main__ directly')
new = tk.Tk()
#new = tk.Toplevel() # this loads an unwanted additional blank window. IF run directly.
page = Page(new)
# the missing line to self contain module!
#page.pack(fill='both', expand=True)
page.grid(row=0,column=0,sticky='nsew')
new.mainloop()
else:
print('running as import with __name__ ==',__name__)
Thanks for your patience and responses. I have re-studied the if main guide you provided a link to, it re-affirms what I already believe I knew about it. A useful example in there for when I want to open a single frame only and switch between them, but in this case I want the main window to remain open whilst calling the modules window.

The problem you're facing is that your Page class isn't written to be reusable. It's relying on global variables and knowledge about the code that is calling it. To be reusable, the Page class needs to be self-contained.
In short, that means that every widget created by Page must be inside the Page class and not in the root window. Also, the code that creates an instance of Page needs to be responsible for calling pack, place, or grid on the instance.
So, the first step is to modify Page so that it can be reusable. The key to that is to require that the caller pass in the parent widget. You're doing that, but you aren't using the passed-in value.
Page should look like the following code. Notice that it explicitly declares parent as the first positional argument, and it passes that on to tk.Frame.__init__:
class Page(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.back_btn = tk.Button(parent, text="close", command=self.back)
...
Notice how the back button is a child of parent rather than the child of some global variable. This is the important step that you are missing, and this is what allows Page to be self-contained and not tightly coupled to the code that creates it.
Once Page is properly self-contained, it is the responsibility of the code that creates an instance of Page to call pack, place, or grid on the instance.
For example, simple_module.py might have this block at the end:
if __name__ == "__main__":
new = tk.Tk()
page = Page(new)
page.pack(fill="both", expand=True)
new.mainloop()
In main.py, since you are importing simple_module as a whole, you need to fully qualify the use of Page:
import simple_module as sm
...
root = tk.Tk()
page = sm.Page(root)
page.pack(fill="both", expand=True)
root.mainloop()
Alternately, you could just import Page and omit the sm.:
from simple_module import Page
...
page = Page(root)
...
Notice how one file can use root and one can use new, but your code will work in either case because it doesn't rely on a global variable. Inside of page it will always be parent no matter what the caller called it.
As an aside, you don't have to import simple_module inside start -- you only need to import it once at the start of the program.

Related

The second time I use a ttk button (in root) to create a toplevel window, the toplevel window cannot be destroyed

Basically, I have a button in root which creates a Toplevel. Then in Toplevel, I also have a ttk.Button, which when clicked will destroy the Toplevel. I am using .destroy(), NOT .quit() or any other wrong commands, which is evident as the first time I create the Toplevel with my root button, my Toplevel button WILL destroy the Toplevel.
However, the second time I click the root button to re-create the Toplevel (in other words, to re-create the Toplevel), the Toplevel will successfully be recreated, but the Toplevel button will not destroy the Toplevel, and I can't understand why. Here is my code (I left in all the additional widgets because when I don't include them, the destroying of the Toplevel works just fine):
from tkinter import *
from tkinter import ttk
class App():
def __init__(self,root):
root.title('My Flashcards')
root.resizable(False,False)
button_create = ttk.Button(root,text='Create New',command=self.create_new).grid(column=1,row=2)
def create_new(self):
self.create_branch = Toplevel()
self.create_branch.resizable(False,False)
self.create_branch.title('Create new flashcards')
ttk.Label(self.create_branch,text='CREATE A NEW FLASHCARD HERE:').grid(column=1,row=1,pady=(10,10),padx=(10,10))
ttk.Label(self.create_branch,text='Type the question:').grid(column=1,row=4,padx=(10,10),pady=(10,10))
self.create_question = Entry(self.create_branch,width=70)
self.create_question.grid(column=1,row=5,padx=(10,10))
ttk.Label(self.create_branch,text='Type the answer:').grid(column=1,row=6,padx=(10,10),pady=(10,10))
self.create_answer = Text(self.create_branch)
self.create_answer.grid(column=1,row=7,padx=(10,10))
self.submit_new_flashcard = ttk.Button(self.create_branch,text='Submit',command=self.submit_new_flashcard)
self.submit_new_flashcard.grid(column=1,row=8,pady=(10,10))
def submit_new_flashcard(self):
#Do some things here with self.create_answer.get() and self.create_question.get()
self.create_branch.destroy()
if __name__ == "__main__":
root = Tk()
App(root)
root.mainloop()
You have a method submit_new_flashcard. When you create the button and assign command=self.submit_new_flashcard the button is created and the command property bound to the method. Then you assign the self.submit_new_flashcard field to be the result of creating that button. When you destroy the button, the reference name is still held in the variable. So in creating the second form you create a button that tries to call the original button name as a function which doesn't do anything but raises a background error.
In short, improved naming and avoiding reuse of the same names in the same scope. eg:
self.submitButton = ttk.Button(..., command=self.submit)
would avoid the issue.

Calling variable from class in seperate python module (tk.Frames)

I'm writing a tkinter app that has 3 pages in three different .py files.
I've reworked the code to allow me to create each frame by running a main overarching app which is self.controller in all the subsequent pages (thanks to some excellent users on this site). The reason I have done this is that I want to be able to pass a user name (tk.StringVar()) from the first Frame to a tk.Label in the second Frame.
As I've said above I've rewritten this code a few times but still when I try to actually call a variable or a function from either of the other pages I get the error shown below.
The other page is called FrontPage and it's stored in front_page.py and when I run that page through the main tk.Tk it works perfectly so I know I have definied self.name_entry properly there.
The (minimum) code I'm using for the GamePage is
import tkinter as tk
from tkinter import SUNKEN
import front_page
from front_page import FrontPage
from character import Character
from dice_roll import Die
class GamePage(tk.Frame):
"""The overall class for the app"""
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="This is the start page")
label.pack(side="top", fill="x", pady=10)
self.features = {}
#This is where I try to call the text from the other page
self.character_name = front_page.FrontPage.name_entry.get()
self.name_label = tk.Label(
self.mainframe,
text= self.character_name,
font=("Courier", 20),
bd = 1,
relief = SUNKEN
)
self.name_label.pack()
When I try to actually call a the text from a tk.Entry on the FrontPage it doesn't work. All the functions from my other classes (which are imported up top) work fine.
Traceback (most recent call last):
File "/Users/kevinomalley/Desktop/python_work/rapid_rpg/app_GUI.py",
line 47, in <module>
app = GUI()
File "/Users/kevinomalley/Desktop/python_work/rapid_rpg/app_GUI.py",
line 20, in __init__
frame = F(parent=container, controller=self)
File "/Users/kevinomalley/Desktop/python_work/rapid_rpg/main_page.py",
line 23, in __init__
self.character_name = front_page.FrontPage.name_entry.get()
AttributeError: type object 'FrontPage' has no attribute 'name_entry'
Now I'm 90% sure this is because I'm not using the self.controller properly
I've seen loads of answers referencing it but no clear explanation about how to use it or effectively call it.
If anyone could free me from 5 days of beating my head against a wall it'd lift my poor little newbie heart.
Thanks
The controller is a way to control access between the pages. You haven't shown all your code, but if you're wanting your pages to be able to access other pages, the first think you need to do is create a function that can return a reference to another page.
For example:
class YourApp(...):
...
def get_page(self, page_class):
return self.frames[page_class]
Now, from any page, you can call this function to get a reference to any other page:
game_page = self.controller.get_page(GamePage)
With that reference, you can now use any of its attributes:. For example:
self.character_name = game_page.name_entry.get()
Note: these examples may not be 100% correct. I don't know how you've implemented the rest of your code. However, the concept is what is important:
add a method to a controller to return a page
call that method to get a reference to a page
use that reference to get attributes of that page
This is all explained in more detail in this answer: https://stackoverflow.com/a/33650527/7432

Need to call class method from different class without initialization of the first class or some other way around it

I have a small problem with my code.
There are two classes. First one creates a window with a Options button. Upon clicking the button, the second class is called and creates another window with an Ok button. Let's say there is also a checkbox, which changes the background color to black or something like that. After clicking the button, whatever changes were made in the options are stored into a file and the second window is closed.
All of this works fine. My problem is that now I need to call method update_init from the first class that will apply those changes to the MainWindow. The code below shows my first solution to this problem, but from what I understand, by using second mainloop I create second thread, which should be avoided.
class MainWindow:
def __init__(self, master):
self.master = master
self.options_btn = tk.Button(self.master, text="Options", command=self.open_options)
self.options_btn.pack()
self.options_window = None
def open_options(self):
options_master = tk.Toplevel()
self.options_window = OptionsWindow(options_master)
options_master.mainloop()
lst = meta_load() # loads changes from a file
self.update_init(lst)
def update_init(self, lst):
#code
class OptionsWindow:
def __init__(self, master):
self.master = master
self.ok_btn = tk.Button(self.master, text="OK", command=self.update_meta)
self.ok_btn.pack()
def update_meta(self):
meta_save(12) # saves changes into a file
self.master.destroy()
main_master = tk.Tk()
main_master.minsize(width=1280, height=720)
b = MainWindow(main_master)
main_master.mainloop()
My second solution was to just put both classes into one, but the code is quite messy if I do so.
Can I somehow call the method update_init (which is in the MainWindow class) from the OptionsWindow class without initializing new MainWindow class window? Or is there any other way to deal with this? I would appreciate any help.
I am sorry if this is too specific, I've tried to make it as general as possible, but it's a very specific problem and I couldn't find much information about it anywhere on the internet.
In general you can call a class method from anywhere you want and pass anything to it without initialisation of that class's instance, thanks to objective nature of python, but beware of self dependencies! Although, I don't think that's a good practice.
class A:
def __init__(self):
self.foo = 'foo'
def return_foo(self):
return self.foo
class B:
def __init__(self):
self.bar = 'bar'
print('Ha-ha Im inited!')
def return_bar(self):
try:
return self.bar
except AttributeError:
return 'bar'
def test():
a = A()
# b = B()
return_bar = getattr(B, 'return_bar', None)
if callable(return_bar):
print('%s%s' % (a.return_foo(), return_bar(None)))
test()
Links:
getattr
callable

PySide/PyQt: Is it possible to make strings that you attach to the QTextBrowser separate clickable units

This might be a silly question but:
When you append a given string to a QTextBrowser object, can you make it a link to a signal to a function that takes its text and does something with it? All I need is for it to save the text to a variable actually.
As in, can a link lead to a function instead of to a website.
It certainly is possible.
Here is a code example:
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
class MainWindow(QtGui.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
main_layout = QtGui.QVBoxLayout()
self.browser = QtGui.QTextBrowser()
self.browser.setHtml('''<html><body>some text<br/>click me to call a function<br/>
Click me to scroll down<br>foo<br>foo<br>foo<br>foo<br>foo<br>foo<br>
foo<a id="my_anchor"></a><br>bar<br>bar<br>bar<br>bar<br>bar<br>bar<br>hello!<br>hello!<br>hello!<br>hello!<br>hello!<br>hello!<br>hello!<br>hello!</body></html''')
self.browser.anchorClicked.connect(self.on_anchor_clicked)
main_layout.addWidget(self.browser)
self.setLayout(main_layout)
def on_anchor_clicked(self,url):
text = str(url.toString())
if text.startswith('some_special_identifier://'):
self.browser.setSource(QtCore.QUrl()) #stops the page from changing
function = text.replace('some_special_identifier://','')
if hasattr(self,function):
getattr(self,function)()
def a_function(self):
print 'you called?'
app = QtGui.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Any link that has a url that begins with "some_special_identifier://" will be picked up and the text after that will be used to look up and call a function of the same name. Please note that this could be a little risky as there is the potential for all sorts of functions to be called that perhaps you don't intend, if the user has any control over what is displayed in the TextBrowser. It might be better to only allow certain functions to be run, and perhaps only at certain times. That is of course up to you to enforce!
P.S. my code is written for Python 2.7 (I see you are using Python 3). So I think you will need to change print 'text' to print('text') at the very least!

Signal/Slot help-- setting a signal to a slot outside of the current class

I'm trying to populate a table (present in the main window) from a slider that's located in a widget in a separate class. I can't seem to get it to work...what's the best way to go about doing this?
Here's my current code:
class Widget(QWidget):
def __init__(self,filename,parent=None):
super(Widget,self).__init__(parent)
self.resize(900,900)
self.layout=QVBoxLayout(self)
frame=Frame(filename)
self.image=pg.ImageView()
self.image.setImage(frame.data)
self.image.setCurrentIndex(0)
fileheader=FileHeader(filename)
self.slider=QSlider(self)
self.slider.setOrientation(Qt.Horizontal)
self.slider.setMinimum(1)
self.slider.setMaximum(fileheader.numframes)
self.slider.sliderMoved.connect(self.sliderMoved)
self.layout.addWidget(self.image)
self.layout.addWidget(self.slider)
def sliderMoved(self,val):
print "slider moved to:", val
fileheader=FileHeader(filename)
idx=val
frame=fileheader.frameAtIndex(idx)
self.image.setImage(frame.data)
class MainWindow(QMainWindow):
def __init__(self, filename, parent=None):
super(MainWindow,self).__init__(parent)
self.initUI(filename)
def initUI(self,filename):
self.filetable=QTableWidget()
self.frametable=QTableWidget()
self.imageBrowser=Widget(filename)
self.imagesplitter=QSplitter(Qt.Horizontal)
self.tablesplitter=QSplitter(Qt.Horizontal)
self.imagesplitter.addWidget(self.imageBrowser)
self.tablesplitter.addWidget(self.imagesplitter)
self.tablesplitter.addWidget(self.filetable)
self.tablesplitter.addWidget(self.frametable)
self.setCentralWidget(self.tablesplitter)
exitAction=QAction(QIcon('exit.png'),'&Exit',self)
exitAction.setShortcut('Ctrl+Q')
exitAction.triggered.connect(qApp.quit)
openAction=QAction(QIcon('open.png'),'&Open',self)
openAction.setShortcut('Ctrl+O')
menubar=self.menuBar()
fileMenu=menubar.addMenu('&File')
fileMenu.addAction(exitAction)
fileMenu.addAction(openAction)
self.fileheader=FileHeader(filename)
self.connect(self.frametable,
SIGNAL("Widget.sliderMoved(idx)"),
self.fileheader.frameAtIndex(idx))
self.frameheader=self.fileheader.frameAtIndex(0)
self.populate()
def populate(self):
self.filetable.setRowCount(len(self.fileheader.fileheader_fields))
self.filetable.setColumnCount(2)
self.filetable.setHorizontalHeaderLabels(['File Header','value'])
for i,field in enumerate(self.fileheader.fileheader_fields):
name=QTableWidgetItem(field)
value=QTableWidgetItem(unicode(getattr(self.fileheader,field)))
self.filetable.setItem(i,0,name)
self.filetable.setItem(i,1,value)
self.frametable.setRowCount(len(self.frameheader.frameheader_fields))
self.frametable.setColumnCount(2)
self.frametable.setHorizontalHeaderLabels(['Frame Header','Value'])
for i,fields in enumerate(self.frameheader.frameheader_fields):
Name=QTableWidgetItem(fields)
Value=QTableWidgetItem(unicode(getattr(self.frameheader,fields)))
self.frametable.setItem(i,0,Name)
self.frametable.setItem(i,1,Value)
I know the "connect" is wrong-- I'm very new to PyQt and Python in general, so I'm not quite sure where to start.
Since self.imageBrowser is your Widget class, it will have the slider attribute which has the sliderMoved signal. You just need a few more dots.
self.imageBrowser.slider.sliderMoved.connect(self.fileheader.frameAtIndex)
The way you have it organized is correct though. Your main window composes your custom widgets and binds the connections together.
Though because you have a data source, and also a QTableWidget that will need to be updated, you probably need to wrap the steps up into a little method:
def initUI(self,filename):
...
self.imageBrowser.slider.sliderMoved.connect(self._handle_slider_moved)
# initialize it the first time during the window set up
self._handle_slider_moved(0)
def _handle_slider_moved(self, val):
# update the data source
self.fileheader.frameAtIndex(val)
# update the second data source
self.frameheader=self.fileheader.frameAtIndex(0)
# now refresh the tables
self.populate()

Resources