I'm learning GTK3 under Python3 and made an app that has only one AppWindow so far.
I'm using Gtk.Application.
I can't figure out how to proper handle opening a second window.
I can open it directly from my main Window but I don't know how to pass Application object to it (I googled and "duckduckgoed" without any success).
Do I need to call Gtk.Application to open second window?
How do I let Gtk.Application track this new window?
Application is like this:
Main window with an objects list from a database.
Second window to edit a single item selected from the main window's
list.
Thanks.
My code (I stripped out the unnecesary code):
# ################################################################
# MAIN APP WINDOW
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Here all the widgets and a button to open the second window
self.show_all()
# ################################################################
# MAIN APP CLASS
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id=MIKUNA_ID,
**kwargs)
self.window = None
def do_startup(self):
Gtk.Application.do_startup(self)
action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.on_quit)
self.add_action(action)
def do_activate(self):
# We only allow a single window and raise any existing ones
if not self.window:
# Windows are associated with the application
# when the last one is closed the application shuts down
self.window = AppWindow(application=self, title="Mikuna")
self.window.present()
def on_quit(self, action, param):
self.quit()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Second window to edit a single item selected from the main window's list.
You just create the second window from your main window then, probably a Gtk.Dialog that is transient to the main window. You only need to make the Application track it if it is a toplevel window you expect to out-live your main window.
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.
Lets say I have 2 windows, one of which opens the other on a menu item click:
class ProjectWindow(QtWidgets.QMainWindow, project_window_qt.Ui_ProjectWindow):
def __init__(self):
super(ProjectWindow, self).__init__()
# Setup the main window UI
self.setupUi(self)
self.new_project_window = None
# Handle menu bar item click events
self.actionNewProject.triggered.connect(self.new_project)
def new_project(self):
self.new_project_window = project_new_window.NewProjectWindow()
self.new_project_window.show()
def refresh_projects(self):
with open(os.path.join(self.directory, 'projects.txt'), 'r') as f:
projects = json.load(f)
return projects
and
class NewProjectWindow(QtWidgets.QDialog, project_new_window_qt.Ui_NewProjectWindow):
def __init__(self,):
super(NewProjectWindow, self).__init__()
# Setup the main window UI
self.setupUi(self)
Once the user closes new_project_window, I want the refresh_projects method to be called in the ProjectWindow class.
I thought about setting up an event listener to check when new_project_window is closed, and then call refresh_projects once that happens, but the window just closes immediately after it opens:
def new_project(self):
self.new_project_window = project_new_window.NewProjectWindow(self.directory, self.project_list)
self.new_project_window.onClose.connect(self.refresh_projects)
self.new_project_window.show()
Is that the correct approach? Or is there a way to call refresh_projects directly from within the new_project_window object?
If you are using QDialog you should call exec_() instead of show(), this will return a value when the user closes the window, and just call refresh project.
def new_project(self):
self.new_project_window = project_new_window.NewProjectWindow(self.directory, self.project_list)
code = self.new_project_window.exec_()
"""code: gets the value of the QDialog.Accepted, or QDialog.Rejected
that you can connect it to some accept button
using the accept() and reject() functions.
"""
self.refresh_projects()
exec_() is blocking, ie the next line is not executed unless the QDialog has been closed.
This is a simplification of an application I wrote.
The application's main window has a button and a checkbox.
The checkbox resides inside a QScrollArea (via a widget).
The checkbox has a number stating how many times that checkbox was created.
Clicking on the button will open a dialog with a refresh button.
Clicking on the refresh button will set a new widget to the scroll area with a new checkbox.
The checkbox has a context menu that opens the same dialog.
However, when the widget is created using the dialog triggered by the context menu of the checkbox the application crashes with the following error:
2016-08-03 09:22:00.036 Python[17690:408202] modalSession has been
exited prematurely - check for a reentrant call to endModalSession:
Python(17690,0x7fff76dcb300) malloc: * error for object
0x7fff5fbfe2c0: pointer being freed was not allocated
* set a breakpoint in malloc_error_break to debug
The crash doesn't happen when clicking on the button to open the dialog and clicking refresh from the dialog.
The crash happens on both Mac and Windows.
I am using Python 2.7.10 with PySide 1.2.4
#!/usr/bin/env python
import sys
from itertools import count
from PySide import QtCore, QtGui
class MainWindow(QtGui.QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.centralwidget = QtGui.QWidget(parent=self)
self.open_diag_btn = QtGui.QPushButton('Open dialog', parent=self)
self.open_diag_btn.clicked.connect(self.open_dialog)
self.scroll_widget = QtGui.QScrollArea(parent=self)
layout = QtGui.QGridLayout(self.centralwidget)
layout.addWidget(self.scroll_widget)
layout.addWidget(self.open_diag_btn)
self.setCentralWidget(self.centralwidget)
self.set_scroll_widget()
def open_dialog(self):
dialog = Dialog(parent=self)
dialog.refresh.connect(self.set_scroll_widget) # Connecting the signal from the dialog to set a new widget to the scroll area
dialog.exec_()
# Even if I call the function here, after the dialog was closed instead of using the signal above the application crashes, but only via the checkbox
# self.set_scroll_widget()
def set_scroll_widget(self):
"""Replacing the widget of the scroll area with a new one.
The checkbox in the layout of the widget has an class instance counter so you can see how many times the checkbox was created."""
widget = QtGui.QWidget()
layout = QtGui.QVBoxLayout(widget)
widget.setLayout(layout)
open_diag_check = RefreshCheckbox(parent=self)
open_diag_check.do_open_dialog.connect(self.open_dialog) # Connecting the signal to open the dialog window
layout.addWidget(open_diag_check)
self.scroll_widget.setWidget(widget)
class RefreshCheckbox(QtGui.QCheckBox):
"""A checkbox class that has a context menu item which emits a signal that eventually opens a dialog window"""
do_open_dialog = QtCore.Signal()
_instance_counter = count(1)
def __init__(self, *args, **kwargs):
super(RefreshCheckbox, self).__init__(unicode(self._instance_counter.next()), *args, **kwargs)
self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
action = QtGui.QAction(self)
action.setText("Open dialog")
action.triggered.connect(self.emit_open_dialog)
self.addAction(action)
def emit_open_dialog(self):
self.do_open_dialog.emit()
class Dialog(QtGui.QDialog):
"""A dialog window with a button that emits a refresh signal when clicked.
This signal is used to call MainWindow.set_scroll_widget()"""
refresh = QtCore.Signal()
def __init__(self, *args, **kwargs):
super(Dialog, self).__init__(*args, **kwargs)
self.refresh_btn = QtGui.QPushButton('Refresh')
self.refresh_btn.clicked.connect(self.do_refresh)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.refresh_btn)
def do_refresh(self):
self.refresh.emit()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
mySW = MainWindow()
mySW.show()
mySW.raise_()
app.exec_()
It looks like Python is trying to delete an object (or one of its child objects) which is no longer there - but quite what causes that to happen is not completely clear to me. The problematic object is the widget set on the scroll-area. If you excplicitly keep a reference to it:
def set_scroll_widget(self):
self._old_widget = self.scroll_widget.takeWidget()
...
your example will no longer dump core.
I think running the dialog with exec may somehow be the proximal cause of the problem, since this means it will run with its own event-loop (which might have an affect on the order of deletion-related events). I was able to find a better fix for your example by running the dialog with show:
def open_dialog(self):
dialog = Dialog(parent=self)
dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose)
dialog.refresh.connect(self.set_scroll_widget)
dialog.setModal(True)
dialog.show()
Doing things this way means it's no longer necessary to keep an explicit reference to the previous scroll-area widget.
I'm trying to launch a dialog window inside of an active application window. The difficulty I am facing is being able to interact with the active application window once the dialog window is launched.
Here is an example of my python script:
class select_output_UI(QtGui.QDialog):
def __init__(self, *args, **kwargs):
super(select_output_UI, self).__init__(*args, **kwargs)
# BUILD UI FROM FILE
ui_file = QtCore.QFile("./select_output.ui")
ui_file.open(QtCore.QFile.ReadOnly)
self.myWidget = QtUiTools.QUiLoader().load(ui_file, self)
ui_file.close()
# SIGNALS
self.myWidget.cancel_button.clicked.connect(self.cancel_button_pressed)
def cancel_button_pressed(self):
self.button_pressed = "CANCEL"
self.close()
dialog = select_output_UI(QtGui.QApplication.activeWindow())
There are 2 options I am familiar with to launch this dialog window:
dialog.show()
This option allow's me to interact with the active application window, but this option will not wait for the dialog window to close before continuing to run whatever code is underneath.
dialog.exec_()
This option does not allow me to interact with the active application window. But what it does do is wait for the dialog window to close before continuing with the rest of the code.
Is there a way to interact with the application window while the dialog window has launch and have python wait till the dialog window is closed before continuing to read the rest of my code?
Sounds like you want to connect your dialog's "OK" (or "proceed", "continue", etc.) button to a method or function containing the rest of the code you want to run. Chances are you'll want it to be a method, since I imagine the rest of the code will need access to some of the widget values on the dialog.
For example:
class select_output_UI(QtGui.QDialog):
def __init__(self, *args, **kwargs):
super(select_output_UI, self).__init__(*args, **kwargs)
# Load .ui file, etc...
self.myWidget.ok_button.clicked.connect(self.do_work)
self.myWidget.cancel_button.clicked.connect(self.reject)
def do_work(self):
print "I'm doing work!"
# Do the work...
self.accept()
dialog = select_output_UI(QtGui.QApplication.activeWindow())
dialog.show()
Alternatively, you could hook your "OK" and "Cancel" buttons up to .accept() and .reject(), respectively, and then attach your do_work() function/method to the dialog's accepted signal. However, if you approach it that way, your code will execute after the dialog is closed, rather than allowing you to close it when you see fit (or, say, leave it open if something goes wrong in the rest of your code).
I have an application design done, without any useful stuff happening, but I have two windows. One is the main window, and one is the about dialogue. They are all on one .ui file from Glade.
When I open the About dialogue using Help>About (in my program), everything works. Closing it and opening it again using the same method results in a blank window.
Here is my code:
#! /usr/bin/env python3
from gi.repository import Gtk
class window():
def __init__(self):
self.builder = Gtk.Builder()
self.builder.add_from_file("./personalinfo.ui")
self.mainWindow = self.builder.get_object("mainWindow")
self.mainWindow.connect("destroy", self.on_mainWindow_destroy)
self.mainWindow.set_title("Persona")
self.mainWindowMainBoxButtonBoxCancel = self.builder.get_object("mainWindowMainBoxButtonBoxCancel")
self.mainWindowMainBoxButtonBoxCancel.connect("clicked", self.on_mainWindowMainBoxButtonBoxCancel_clicked)
self.mainWindowMainBoxButtonBoxOK = self.builder.get_object("mainWindowMainBoxButtonBoxOK")
self.mainWindowMainBoxButtonBoxOK.connect("clicked", self.on_mainWindowMainBoxButtonBoxOK_clicked)
self.mainWindowMainBoxGenderBoxCombo = self.builder.get_object("mainWindowMainBoxGenderBoxCombo")
self.mainWindowMainBoxGenderBoxCombo.set_active(0)
self.mainWindowMainBoxMenuFileMenuQuit = self.builder.get_object("mainWindowMainBoxMenuFileMenuQuit")
self.mainWindowMainBoxMenuFileMenuQuit.connect("activate", self.on_mainWindowMainBoxMenuFileMenuQuit_activate)
self.mainWindowMainBoxMenuHelpMenuAbout = self.builder.get_object("mainWindowMainBoxMenuHelpMenuAbout")
self.mainWindowMainBoxMenuHelpMenuAbout.connect("activate", self.on_mainWindowMainBoxMenuHelpMenuAbout_activate)
self.mainWindow.show_all()
def on_mainWindow_destroy(self, widget):
print("destroy: 'mainWindow'")
Gtk.main_quit()
def on_mainWindowMainBoxButtonBoxCancel_clicked(self, widget):
print("clicked: 'mainWindowMainBoxButtonBoxCancel'")
Gtk.main_quit()
def on_mainWindowMainBoxButtonBoxOK_clicked(self, widget):
print("clicked: 'mainWindowMainBoxButtonBoxOK'")
Gtk.main_quit()
def on_mainWindowMainBoxMenuFileMenuQuit_activate(self, widget):
print("activate: 'mainWindowMainBoxMenuFileMenuQuit'")
Gtk.main_quit()
def on_mainWindowMainBoxMenuHelpMenuAbout_activate(self, widget):
print("activate: 'mainWindowMainBoxMenuHelpMenuAbout'")
self.aboutWindow = self.builder.get_object("aboutWindow")
self.aboutWindow.set_title("About Persona")
self.aboutWindowMainBoxButtonBoxOK = self.builder.get_object("aboutWindowMainBoxButtonBoxOK")
self.aboutWindowMainBoxButtonBoxOK.connect("clicked", self.on_aboutWindowMainBoxButtonBoxOK_clicked)
self.aboutWindow.show_all()
def on_aboutWindowMainBoxButtonBoxOK_clicked(self, widget):
print("clicked: 'aboutWindowMainBoxButtonBoxOK'")
self.aboutWindow.destroy()
w = window()
Gtk.main()
The About window isn't created each time you call self.aboutWindow = self.builder.get_object("aboutWindow") it is created once when you load the UI file. So when you call self.aboutWindow.destroy() it destroys it forever, and the next time you try to display the window it no longer exists.
Instead of destroying the window, you probably want to hide it instead self.aboutWindow.hide()