I am writing a class that I want to include multiple widgets that can be displayed in a Jupyter notebook. These widgets should calls class methods that update class parameters. A function that I connect to an ipywidget's events need access to the class instance, I think through self, but I can't figure out how to get this communication to work.
Here's a minimal example:
import numpy as np
import ipywidgets as widgets
class Test(object):
def __init__(self):
self.val = np.random.rand()
display(self._random_button)
_random_button = widgets.Button(
description='randomize self.val'
)
def update_random(self):
self.val = np.random.rand()
print(self.val)
def button_pressed(self):
self.update_random()
_random_button.on_click(button_pressed)
I see how the button_pressed() function sees the Button instance as self, giving "AttributeError: 'Button' object has no attribute 'update_random'".
Is there a way that I can access methods of the class Test through a button that belongs to the class, or is there a better way that I should be structuring this code to ease communication between these components?
The button widget and the on_click should be created (or initialised) in the init method.
The on_click method generates an argument that is sent to the function, but it is not needed in this case so I have just put a *args in the button_pressed function.
The display call is not needed.
When calling a function in a class, you must use self.functionName. That includes the function calls in on_click or observe
In this case, you didn't need the random number generation in the init function.
There are a few examples of Jupyter widgets within classes here: https://github.com/bloomberg/bqplot/tree/master/examples/Applications
import numpy as np
import ipywidgets as widgets
class Test(object):
def __init__(self):
self.random_button = widgets.Button(
description='randomize self.val')
self.random_button.on_click(self.button_pressed)
def update_random(self):
self.val = np.random.rand()
print(self.val)
def button_pressed(self,*args):
self.update_random()
buttonObject = Test()
# display(buttonObject.random_button) # display works but is not required if on the last line in Jupyter cell.
buttonObject.random_button # Widget to be dispalyed - must last last line in cell
When using JupyterLab, if you want the output to show in the notebook cell, rather than in the notebook log, a minor tweak is needed to #DougR's excellent answer:
import numpy as np
import ipywidgets as widgets
# create an output widget
rand_num_output = widgets.Output()
class Test(object):
def __init__(self):
self.random_button = widgets.Button(
description='randomize self.val')
self.random_button.on_click(self.button_pressed)
def update_random(self):
# clear the output on every click of randomize self.val
rand_num_output.clear_output()
# execute function so it gets captured in output widget view
with rand_num_output:
self.val = np.random.rand()
print(self.val)
def button_pressed(self,*args):
self.update_random()
buttonObject = Test()
# display(buttonObject.random_button) # display works but is not required if on the last line in Jupyter cell.
widgets.HBox([buttonObject.random_button, rand_num_output]) # Widget to be dispalyed - must last last line in cell, add output widget
Related
i have a main GUI-Window from which i open a new Window (FCT-popup) with a buttonclick:
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.ui = Ui_MainWindow() # sets ui = to the main window from the ui-file
self.ui.setupUi(self)
[...]
def enter_fct_results(self):
self.FCTpopup = FCT_Window()
self.FCTpopup.show()
In the Window i have a QTable to fill and a button to submit the data and close the window:
class FCT_Window(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.ui = Ui_FCT_Window()
self.ui.setupUi(self)
[...]
self.ui.pushButton_submitFCT.clicked.connect(lambda: MainWindow.store_fct_data(MainWindow, self.on_submit()[0]))
def on_submit(self): # event when user clicks
fct_nparray = np.zeros((self.ui.tableFCTinputs.rowCount(), self.ui.tableFCTinputs.columnCount()))
for j in range(self.ui.tableFCTinputs.columnCount()):
for i in range(self.ui.tableFCTinputs.rowCount()):
fct_nparray[i, j] = float(self.ui.tableFCTinputs.item(i, j).text())
return fct_nparray, lambda: self.close()
self.ui.pushButton_submitFCT.clicked.connect(lambda: MainWindow.store_fct_data(MainWindow, self.on_submit()[0]))
The receiving function iin the main window looks like ths:
def store_fct_data(self, data):
self.fct_data = data
Now i just want to understand how i can make either the mainwindow or the pushbutton which opens the 2nd window disabled. Disabling inside enter_fct_results() works, but if i want to enable it again with either store_fct_data or on_submit provides errors like this:
self.ui.pushButton_FCTresults.setEnabled(1)
self.ui.pushButton_submitFCT.clicked.connect(lambda: MainWindow.store_fct_data(MainWindow, self.on_submit()[0]))
AttributeError: type object 'MainWindow' has no attribute 'ui'
I dont think i have understood it here how to deal with multiple windows and stuff. For example how would i change a the color of a button in the main window by using a button in window2. How do i access the widgets? if i am inside the same Window i do that easily by
self.ui.pushbutton.setText("New Text")
I dont get how to access items and attributes across Windows :( Can you help me?
Access to attributes of another instance
There is a fundamental difference between disabling the button of the second window in enter_fct_results and what you tried in the lambda: in the first case, you're accessing an instance attribute (for instance, self.FCTpopup.ui.pushButton), while in the second you're trying to access a class attribute.
The self.ui object is created in the __init__ (when the class instance is created), so the instance will have an ui attribute, not the class:
class Test:
def __init__(self):
self.value = True
test = Test()
print(test.value)
>>> True
print(Test.value)
>>> AttributeError: type object 'Test' has no attribute 'value'
Provide a reference
The simple solution is to create a reference of the instance of the first window for the second:
def enter_fct_results(self):
self.FCTpopup = FCT_Window(self)
self.FCTpopup.show()
class FCT_Window(QMainWindow):
def __init__(self, mainWindow):
QMainWindow.__init__(self)
self.mainWindow = mainWindow
self.ui.pushButton_submitFCT.clicked.connect(self.doSomething)
def doSomething(self):
# ...
self.mainWindow.ui.pushButton.setEnabled(True)
Using modal windows (aka, dialogs)
Whenever a window is required for some temporary interaction (data input, document preview, etc), a dialog is preferred: the main benefit of using dialogs is that they are modal to the parent, preventing interaction on that parent until the dialog is closed; another benefit is that (at least on Qt) they also have a blocking event loop within their exec() function, which will only return as soon as the dialog is closed. Both of these aspects also make unnecessary disabling any button in the parent window. Another important reason is that QMainWindow is not intended for this kind of operation, also because it has features that generally unnecessary for that (toolbars, statusbars, menus, etc).
def enter_fct_results(self):
self.FCTpopup = FCT_Window(self)
self.FCTpopup.exec_()
class FCT_Window(QDialog):
def __init__(self, parent):
QMainWindow.__init__(self, parent)
self.ui.pushButton_submitFCT.clicked.connect(self.doSomething)
def doSomething(self):
# ...
self.accept()
The above makes mandatory to recreate the ui in designer using a QDialog (and not a QMainWindow) instead. You can just create a new one and drag&drop widgets from the original one.
i finally found my mistake: It was the place of the signal connection. It has to be right before the 2nd window is opened:
def enter_fct_results(self):
self.FCTpopup = Dialog(self.fct_data)
self.FCTpopup.submitted.connect(self.store_fct_data)
self.FCTpopup.exec_()
With this now i can send the stored data from the mainwindow to the opened window, import into the table, edit it and send it back to the main window on submit.
Using jupyterlab 1.1.4 I want to create an object that displays a widget and that simplifies the definition of the widgets and callback functions. In a first cell I create and start the instance:
from ipywidgets import FileUpload
from ipywidgets import widgets
from IPython.display import display
class MyWidget():
def __init__(self):
self.output = widgets.Output()
self.upload = FileUpload(description='Choose file')
self.upload_filename = None
self.upload.observe(self.upload_file, names=['value'])
display(self.upload, self.output)
#output.capture()
def upload_file(self, args):
self.upload_filename = list(args['new'].keys())[0]
print("File selected: {}".format(self.upload_filename))
w = MyWidget()
When running this cell it shows a FileUpload button, and when I click on it I can choose a file. But when I do so, I expect to see some output (as I print the selected filename). But there is no output.
What am I doing wrong? How to do it correctly?
I even can go farther and change the upload_file method like the following:
#output.capture()
def upload_file(self, args):
a=1/0
self.upload_filename = list(args['new'].keys())[0]
print("File selected: {}".format(self.upload_filename))
which contains an obvious error! But when I run the cell and choose a file, I do not see any error output.
How to fix this?
I am building a data-analysis program using Python and Tkinter that allows for the entry, plotting and export of data (an Excel clone of sorts). It implements many windows and functions that need to access and modify shared data variables. How should I organize the GUI in order to ensure that each window can transfer data back and forth, while also compartmentalizing the code to be able to modify and debug each function independently?
I initially built a main GUI class with sub-functions (Example 1), where it is easy for all of the functions to modify and access the data initialized, as all variables are shared through self. However, as I increasingly modify the code (which is actually thousands of lines long), it became challenging to debug as the code isn't compartmentalized well.
I saw a suggestion to have each window (ie data table, plot window, etc) be independent classes (Example 2), but the only way I figured out how to pass the data between classes (passing the first window object to the other windows) seems very messy, and the only alternative I could figure out was explicitly passing all necessary variables to each window at the definition/call, which can also be very messy.
Example 1
class GUI:
def __init__(self, toptk):
self.toptk = toptk
""" Code creating main window and toolbar """
list_x = []
list_y = []
""" Code adding data """
def entryTable(self):
tablewin = Toplevel(self.toptk)
""" Code creating frames, buttons, etc """
""" Code doing something to list_x, list_y """
def plotWin(self):
plotwin = Toplevel(self.toptk)
""" Code creating frames, buttons, etc """
""" Code doing something to list_x, list_y """
entryTable()
plotWin()
root = tk.Tk()
main = GUI(root)
root.mainloop()
Example 2
class GUI:
def __init__(self, toptk):
""" Code creating main window and toolbar """
list_x = []
list_y = []
""" Code adding data """
entryTable.__init__(toptk,self)
plotWin.__init__(toptk,self)
class entryTable():
def __init__(self,toptk,topGUI):
tabletop = Toplevel(toptk)
""" Code creating frames, buttons, etc """
""" Code doing something to topGUI.list_x, topGUI.list_y """
class plotWin():
def __init__(self,toptk,topGUI):
plottop = Toplevel(toptk)
""" Code creating frames, buttons, etc """
""" Code doing something to topGUI.list_x, topGUI.list_y """
How can I improve the organization of this multi-window program in a way that retains a simple exchange of variables between classes while also allowing me to isolate and debug each one individually?
Thank you very much.
Question: how to pass the data between classes
This solution don't pass anything beteen classes, it simple uses a global class DATA object. It's also possible to hold this object in a own DATA.py, to do import DATA.
import tkinter as tk
class DATA:
""" Data container """
list_x = []
list_y = []
class EntryTable(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
""" Code creating frames, buttons, etc """
""" Code adding something to DATA.list_x, DATA.list_y """
DATA.list_x = [1, 2, 3]
DATA.list_y = [4, 5, 6]
class PlotWin(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
""" Code creating frames, buttons, etc """
""" Code doing something with DATA.list_x, DATA.list_y """
print('plot:{}'.format((DATA.list_x, DATA.list_y)))
class App(tk.Tk):
def __init__(self):
super().__init__()
""" Code adding main window widgets and toolbar """
""" Code adding Toplevel window's """
entryTable = EntryTable(self)
plotWin = PlotWin(self)
if __name__ == '__main__':
App().mainloop()
I'm making a large program in Python and using PyQt for the GUI. The whole program is divided into different modules so that different people can work on it simultaneously without interfering with the other people's work.
I am working on 3 different modules right now. 1 is the main program window that handles the basic UI and assigns widgets so the main window (this is not important, just so you know why the code doesn't look like a full program.)
First is the widget:
import sys
from PyQt4 import QtCore
from PyQt4 import QtGui
from CustomButton import HoverButton #just a custom button class
from CustomGif import LblLoadingGif #where things go wrong
class Page1(QtGui.QWidget):
def __init__(self, parent=None):
super(Page1, self).__init__(parent)
self.lbl1GIF = LblLoadingGif(self)
self.lbl1GIF.move(400, 45)
self.btnStart = HoverButton(self)
self.btnStart.setText('Start')
self.btnStart.move(35, 400)
self.btnStart.clicked.connect(self.actStartGif)
#the code below works, but then I can only perform 1 action with each button
#self.btnStart.clicked.connect(self.lbl1GIF.actStart)
def actStartGif(self):
self.lbl1GIF.actStart
The code for the custom GIF looks as follows:
import sys
from PyQt4 import QtCore
from PyQt4 import QtGui
class LblLoadingGif(QtGui.QLabel):
def __init__(self, parent=None):
QtGui.QLabel.__init__(self, parent)
self.setStyleSheet('background: url();')
self.setScaledContents(True)
self.resize(100, 100)
self.movLoadGif = QtGui.QMovie('Resources_Images/Loading6.gif', QtCore.QByteArray())
self.movLoadGif.setCacheMode(QtGui.QMovie.CacheAll)
self.movLoadGif.setSpeed(100)
self.setMovie(self.movLoadGif)
self.hide()
def actStart(self, event):
#print('test1')
self.show()
self.movLoadGif.start()
def actStop(self, event):
#print('test1')
self.hide()
self.movLoadGif.stop()
So the problem is that I can use the actStart function just fine when I call it from the button click directly, but not when I call it through another function. I have used a lot of different variations of brackets, self, Page1 when calling the actStart of the custom gif from withing the actStartGif function.
Any help will be appreciated.
When you use connect it is necessary to pass the name of the function since internally it is in charge of calling it, in your case you have to call it directly so you will have to pass its parameters, in this case event:
self.lbl1GIF.actStart({your value for event})
I do not understand why you use event for what happens to you None:
def actStartGif(self):
self.lbl1GIF.actStart(None)
I have this strange situation with PySide/Qt.
EDIT: I added a small "can run" code to test the experienced behavior, at the end. If you want, you can skip the babbling and see what I am talking about through actual code. It should behave unexpectedly according to the signal/slot mechanism
Quick presentation of the design. I have a MainWindow, which holds two views, two controllers and two models.
The two views are created in the MainWindow, like this
ui.listView = views.ListView(ui.hSplitter)
ui.threeDView = views.ThreeDView(ui.hSplitter)
Nothing fancy. The models are created after the ui
listModel = models.ListModel()
listSelectionModel = models.ListSelectionModel()
The first one contains the actual data. The second one contains the selection, so that the 3D view can check if the selection has changed (normally by the user clicking on the listView) and plot the entity in 3d.
Finally, the controller are created like this
threeDController = ThreeDController(threeDView=self._ui.threeDView,
listModel=listModel,
listSelectionModel=listSelectionModel)
listController = ListController(listView = self._ui.listView,
listModel = listModel,
listSelectionModel=listSelectionModel)
Then I add some mock items in the listModel to have something to click.
The idea is that the list should show the items, and when you click, the 3d view will display them. I did as follows: ListView is defined as this
class ListView(QtGui.QWidget):
itemActivated = QtCore.Signal()
def __init__(self, parent):
super(ListView, self).__init__(parent)
self._ui = self._createUi()
self._ui.qListWidget.itemActivated.connect(self._itemActivatedSlot)
def _createUi(self):
ui = UIElems()
hLayout = QtGui.QHBoxLayout(self)
ui.qListWidget = QtGui.QListWidget(self)
hLayout.addWidget(ui.qListWidget)
return ui
def _itemActivatedSlot(self, listitem):
# I CONFIRM THIS LINE IS EXECUTED
# SO THE EMIT IS CORRECTLY PERFORMED
self.itemActivated.emit()
Note how I defined an itemActivated signal for the ListView. I intercept the QListWidget (note the Q) itemActivated, redirect it to a protected slot of ListView, and then emit a ListView signal, which will be listened externally. I do this because I want my ListView to wrap completely its internal controls.
Who is listening to this ListView itemActivated event? The ListController is, which should get this signal, and set the selection accordingly, so that the 3d controller can update the 3d view.
This is the ListController
class ListController(QtCore.QObject):
def __init__(self, listView, listModel, selectionModel):
super(ListController, self).__init__()
self._listView = listView
self._listModel = listModel
self._selectionModel = selectionModel
self._listModel.changed.connect(self._listModelChanged)
# HERE IS THE PROBLEM
self._listView.itemActivated.connect(self._listViewItemActivated)
# PLACEHOLDER
def _listModelChanged(self):
self._listView.setItems(self._listModel.items())
def _listViewItemActivated(self): ### ... AND HERE
identifier = self._listView.currentSelectedId()
self._selectionModel.setSelected(identifier)
As you can see, in the controller, I connect to the ListView's signal itemActivated, and expect to see the proper slot called. This unfortunately does not happen. If I, however, add the following line at the placeholder
self._listView.itemActivated.emit()
that is, I force the emitting right after the connect "from outside" (I am in the controller, and I emit a signal defined in the view), I start receiving the events and everything works as expected: the ListController._listViewItemActivated is called every time I click on the items, and the 3d view is consequently updated.
I tried to replicate the case in a smaller program, but I always got something that works. At this point, you are my only hope, because the behavior I experience makes no sense at all.
EDIT
This code shows the same behavior, except that now even the forced emit does not solve. Try to click the "hello" element in the listview. It will print the message, but the emit will not follow and the "Victory!" line will not be printed.
import sys
from PySide import QtGui, QtCore
class ListView(QtGui.QWidget):
itemActivated = QtCore.Signal()
def __init__(self, parent):
super(ListView, self).__init__(parent)
qlistview = QtGui.QListWidget(self)
qlistview.addItem("hello")
qlistview.itemActivated.connect(self._itemActivatedSlot)
def _itemActivatedSlot(self, listitem):
print "Activated "+ listitem.text() # this is printed.
self.itemActivated.emit() # Why not emitted or received ?!?!
class ListController(QtCore.QObject):
def __init__(self, listView):
super(ListController, self).__init__()
self._listView = listView
self._listView.itemActivated.connect(self._listViewItemActivated)
# self._listView.itemActivated.emit() # uncomment to see that the signal is actually connected and the mechanism works
def _listViewItemActivated(self):
print "_listViewItemActivated activated!!! Victory!"
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
listView = ListView(self)
listController = ListController(listView)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
app.setApplicationName("Test")
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
I might be wrong, but try to add slot decorator like:
#QtCore.Slot()
def _listViewItemActivated(self):
identifier = self._listView.currentSelectedId()
self._selectionModel.setSelected(identifier)
Or maybe just adding some dummy parameter to pass?
#QtCore.Slot(int)
...
itemActivated = QtCore.Signal(int)
Ok, the problem was subtle, and I'm a fool not realizing it, because it's basic python after all. You have to store the controllers somewhere on the MainWindow object, otherwise as soon as init is over, they will go out of scope and disappear, so they won't listen to signals anymore.