0

I am making a text editor and I want to implement a confirmation before closing the program if the text from the editor has not been saved.

I wrote the close event instruction.

If I wrote a text and didn't save it, then when I try to close the window I get a message with a suggestion to save the text.

If I click Don't Save or Cancel in the dialog box that appears, then the code works fine. If I click Save, then I get an error message. error message

In addition, the code in closeEvent assumes that if the text has been changed in an open text file, then it will simply save it to an existing file, and if the file has not been previously saved, then QFileDialog will be called. But my code calls QFileDialog anyway.

Although I use the code from closeEvent for the Create and Exit button and it works well there.

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QFileDialog, QMessageBox
from PyQt5.QtCore import QFileInfo


class Ui_MyNotepad(QtWidgets.QMainWindow):
    def __init__(self, parent = None):
        super().__init__()
        self.file_path = ""
    def setupUi(self, MyNotepad):
        MyNotepad.setObjectName("MyNotepad")
        MyNotepad.resize(815, 538)
        MyNotepad.setStyleSheet("QTextEdit {\n"
"    background-color: white;\n"
"}\n"
"QScrollBar {\n"
"    background-color: white;\n"
"    height: 8;\n"
"    width: 8;\n"
"}\n"
"QScrollBar:handle {\n"
"    background-color: #fae0e4;\n"
"}\n"
"QScrollBar:handle:pressed {\n"
"    background-color: #fbb1bd;\n"
"}\n"
"QLabel, QComboBox#bar_encoding {\n"
"    background-color: #F7F5FB;\n"
"    border: 1px solid silver;\n"
"    padding-left: 5;\n"
"}\n"
"QComboBox#bar_encoding:drop-down \n"
"{\n"
"    width: 0px;\n"
"    height: 0px;\n"
"    border: 0px;\n"
"}\n"
"QComboBox#bar_encoding:hover {\n"
"    background-color: #F9DBBD;\n"
"}\n"
"QMenuBar {\n"
"    background-color: white;\n"
"}")
        self.centralwidget = QtWidgets.QWidget(MyNotepad)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout.setContentsMargins(0, 1, 0, 0)
        self.verticalLayout.setSpacing(0)
        self.verticalLayout.setObjectName("verticalLayout")
        self.work_space = QtWidgets.QTextEdit(self.centralwidget)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.work_space.sizePolicy().hasHeightForWidth())
        self.work_space.setSizePolicy(sizePolicy)
        font = QtGui.QFont()
        font.setKerning(True)
        font.setStyleStrategy(QtGui.QFont.PreferDefault)
        self.work_space.setFont(font)
        self.work_space.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
        self.work_space.setAcceptDrops(True)
        self.work_space.setStatusTip("")
        self.work_space.setAutoFillBackground(False)
        self.work_space.setInputMethodHints(QtCore.Qt.ImhNone)
        self.work_space.setFrameShape(QtWidgets.QFrame.NoFrame)
        self.work_space.setFrameShadow(QtWidgets.QFrame.Plain)
        self.work_space.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
        self.work_space.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
        self.work_space.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustIgnored)
        self.work_space.setAutoFormatting(QtWidgets.QTextEdit.AutoNone)
        self.work_space.setLineWrapMode(QtWidgets.QTextEdit.NoWrap)
        self.work_space.setLineWrapColumnOrWidth(10)
        self.work_space.setAcceptRichText(True)
        self.work_space.setObjectName("work_space")
        self.verticalLayout.addWidget(self.work_space)
        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
        self.verticalLayout.addItem(spacerItem)
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)
        self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout.setSpacing(0)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.bar_zoom = QtWidgets.QLabel(self.centralwidget)
        font = QtGui.QFont()
        font.setFamily("Verdana")
        font.setPointSize(9)
        self.bar_zoom.setFont(font)
        self.bar_zoom.setStyleSheet("")
        self.bar_zoom.setObjectName("bar_zoom")
        self.horizontalLayout.addWidget(self.bar_zoom)
        spacerItem1 = QtWidgets.QSpacerItem(0, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem1)
        self.bar_position = QtWidgets.QLabel(self.centralwidget)
        font = QtGui.QFont()
        font.setFamily("Verdana")
        font.setPointSize(9)
        self.bar_position.setFont(font)
        self.bar_position.setObjectName("bar_position")
        self.horizontalLayout.addWidget(self.bar_position)
        spacerItem2 = QtWidgets.QSpacerItem(0, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem2)
        self.bar_byte_code = QtWidgets.QLabel(self.centralwidget)
        font = QtGui.QFont()
        font.setFamily("Verdana")
        font.setPointSize(9)
        self.bar_byte_code.setFont(font)
        self.bar_byte_code.setFocusPolicy(QtCore.Qt.NoFocus)
        self.bar_byte_code.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
        self.bar_byte_code.setLayoutDirection(QtCore.Qt.LeftToRight)
        self.bar_byte_code.setAutoFillBackground(False)
        self.bar_byte_code.setInputMethodHints(QtCore.Qt.ImhNone)
        self.bar_byte_code.setFrameShape(QtWidgets.QFrame.NoFrame)
        self.bar_byte_code.setFrameShadow(QtWidgets.QFrame.Plain)
        self.bar_byte_code.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
        self.bar_byte_code.setObjectName("bar_byte_code")
        self.horizontalLayout.addWidget(self.bar_byte_code)
        self.bar_encoding = QtWidgets.QComboBox(self.centralwidget)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.bar_encoding.sizePolicy().hasHeightForWidth())
        self.bar_encoding.setSizePolicy(sizePolicy)
        font = QtGui.QFont()
        font.setFamily("Verdana")
        font.setPointSize(9)
        self.bar_encoding.setFont(font)
        self.bar_encoding.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
        self.bar_encoding.setEditable(False)
        self.bar_encoding.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContentsOnFirstShow)
        self.bar_encoding.setIconSize(QtCore.QSize(20, 20))
        self.bar_encoding.setDuplicatesEnabled(False)
        self.bar_encoding.setFrame(True)
        self.bar_encoding.setObjectName("bar_encoding")
        self.bar_encoding.addItem("")
        self.bar_encoding.addItem("")
        self.bar_encoding.addItem("")
        self.bar_encoding.addItem("")
        self.bar_encoding.addItem("")
        self.horizontalLayout.addWidget(self.bar_encoding)
        spacerItem3 = QtWidgets.QSpacerItem(0, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem3)
        self.horizontalLayout.setStretch(0, 850)
        self.horizontalLayout.setStretch(2, 60)
        self.horizontalLayout.setStretch(4, 60)
        self.horizontalLayout.setStretch(6, 100)
        self.verticalLayout.addLayout(self.horizontalLayout)
        self.verticalLayout.setStretch(0, 10000)
        self.verticalLayout.setStretch(1, 1)
        self.verticalLayout.setStretch(2, 250)
        self.work_space.raise_()
        MyNotepad.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MyNotepad)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 815, 26))
        self.menubar.setAutoFillBackground(False)
        self.menubar.setNativeMenuBar(True)
        self.menubar.setObjectName("menubar")
        self.file = QtWidgets.QMenu(self.menubar)
        self.file.setObjectName("file")
        MyNotepad.setMenuBar(self.menubar)
        self.create = QtWidgets.QAction(MyNotepad)
        self.create.setCheckable(False)
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap(":/white-theme/DATA/images/white theme/create.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.create.setIcon(icon)
        self.create.setStatusTip("")
        self.create.setWhatsThis("")
        self.create.setObjectName("create")
        self.save = QtWidgets.QAction(MyNotepad)
        icon3 = QtGui.QIcon()
        icon3.addPixmap(QtGui.QPixmap(":/white-theme/DATA/images/white theme/save.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.save.setIcon(icon3)
        self.save.setObjectName("save")
        self.save_as = QtWidgets.QAction(MyNotepad)
        icon4 = QtGui.QIcon()
        icon4.addPixmap(QtGui.QPixmap(":/white-theme/DATA/images/white theme/save-as.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.save_as.setIcon(icon4)
        self.save_as.setObjectName("save_as")
        self.exit = QtWidgets.QAction(MyNotepad)
        self.exit.setObjectName("exit")
        self.file.addAction(self.create)
        self.file.addAction(self.save)
        self.file.addAction(self.save_as)
        self.file.addSeparator()
        self.file.addSeparator()
        self.file.addAction(self.exit)
        self.menubar.addAction(self.file.menuAction())

        self.retranslateUi(MyNotepad)
        self.bar_encoding.setCurrentIndex(3)
        QtCore.QMetaObject.connectSlotsByName(MyNotepad)

        self.add_functions()
        self.work_space.textChanged.connect(self.text_changed)

    def retranslateUi(self, MyNotepad):
        _translate = QtCore.QCoreApplication.translate
        MyNotepad.setWindowTitle(_translate("MyNotepad", "Безымянный - MyNotepad"))
        self.bar_zoom.setText(_translate("MyNotepad", "Стр1, стлб 1"))
        self.bar_position.setText(_translate("MyNotepad", "100%"))
        self.bar_byte_code.setText(_translate("MyNotepad", "Windows (CRLF)"))
        self.bar_encoding.setItemText(0, _translate("MyNotepad", "ANSI"))
        self.bar_encoding.setItemText(1, _translate("MyNotepad", "UTF-16 LE"))
        self.bar_encoding.setItemText(2, _translate("MyNotepad", "UTF-16 BE"))
        self.bar_encoding.setItemText(3, _translate("MyNotepad", "UTF-8"))
        self.bar_encoding.setItemText(4, _translate("MyNotepad", "UTF-8 со спецификацией"))
        self.file.setTitle(_translate("MyNotepad", "Файл"))
        self.create.setText(_translate("MyNotepad", "Создать"))
        self.create.setShortcut(_translate("MyNotepad", "Ctrl+N"))
        self.save.setText(_translate("MyNotepad", "Сохранить"))
        self.save.setShortcut(_translate("MyNotepad", "Ctrl+S"))
        self.save_as.setText(_translate("MyNotepad", "Сохранить как..."))
        self.save_as.setShortcut(_translate("MyNotepad", "Ctrl+Shift+S"))
        self.exit.setText(_translate("MyNotepad", "Выход"))

    def add_functions(self):
        self.create.triggered.connect(self.create_new)
        self.save.triggered.connect(self.save_file)
        self.save_as.triggered.connect(self.save_as_file)
        self.exit.triggered.connect(self.exit_program)

    def text_changed(self):
        if MyNotepad.windowTitle()[0] != "*":
            MyNotepad.setWindowTitle("*" + MyNotepad.windowTitle())

    def question_save(self):
        self.file_name = MyNotepad.windowTitle().replace(" - MyNotepad", "").replace("*", "")
        self.msgBox = QMessageBox()
        self.msgBox.setWindowTitle("MyNotepad")
        self.msgBox.setWindowFlags(QtCore.Qt.SubWindow)
        self.msgBox.setText("Вы хотите сохранить измнения в файле\n"
                       + f"\"{self.file_name}\"")
        self.buttonSave = self.msgBox.addButton("Сохранить", QMessageBox.YesRole)
        self.buttonDontSave = self.msgBox.addButton("Не сохранять", QMessageBox.NoRole)
        self.buttonCancel = self.msgBox.addButton("Отменить", QMessageBox.RejectRole)
        self.msgBox.setDefaultButton(self.buttonSave)
        self.msgBox.exec_()


    def create_new(self):
        if MyNotepad.windowTitle()[0] == "*":
            self.question_save()
            if self.msgBox.clickedButton() == self.buttonSave:
                if self.file_path == "":
                    self.save_as_file()
                    if self.file_path != "":
                        self.bar_encoding.setCurrentIndex(2)
                        self.file_path == ""
                        self.work_space.clear()
                        MyNotepad.setWindowTitle("Безымянный - MyNotepad")
                elif self.file_path != "":
                    self.bar_encoding.setCurrentIndex(2)
                    self.save_file()
                    self.file_path == ""
                    self.work_space.clear()
                    MyNotepad.setWindowTitle("Безымянный - MyNotepad")
        if MyNotepad.windowTitle()[0] != "*" or self.msgBox.clickedButton() == self.buttonDontSave:
            self.bar_encoding.setCurrentIndex(2)
            self.file_path = ""
            self.work_space.clear()
            MyNotepad.setWindowTitle("Безымянный - MyNotepad")

    def save_file(self):
        if self.file_path == "":
            self.save_as_file()
        elif self.file_path != "":
            self.file = open(self.file_path, "w", encoding = self.bar_encoding.currentText())
            text = self.work_space.toPlainText()
            self.file.write(text)
            self.file.close()
            MyNotepad.setWindowTitle(QFileInfo(self.file_path).fileName() + " - MyNotepad")

    def save_as_file(self):
        self.file_path = QFileDialog.getSaveFileName(None, "Save as...", None,
                                                "Текстовые файлы (*.txt);;All files (*.*)")[0]
        try:
            self.file = open(self.file_path, "w", encoding = self.bar_encoding.currentText())
            text = self.work_space.toPlainText()
            self.file.write(text)
            self.file.close()
            MyNotepad.setWindowTitle(QFileInfo(self.file_path).fileName() + " - MyNotepad")
        except FileNotFoundError:
            self.file_path == ""

    def exit_program(self):
        if MyNotepad.windowTitle()[0] == "*":
            QtCore.QMetaObject.connectSlotsByName(self.question_save())
            if self.msgBox.clickedButton() == self.buttonSave:
                if self.file_path == "":
                    QtCore.QMetaObject.connectSlotsByName(self.save_as_file())
                    if self.file_path != "":
                        sys.exit()
                elif self.file_path != "":
                    QtCore.QMetaObject.connectSlotsByName(self.save_file())
                    sys.exit()
            elif self.msgBox.clickedButton() == self.buttonDontSave:
                sys.exit()
        elif MyNotepad.windowTitle()[0] != "*":
            sys.exit()

    def closeEvent(self, event:QtGui.QCloseEvent):
        if MyNotepad.windowTitle()[0] == "*":
            self.question_save()
            if self.msgBox.clickedButton() == self.buttonSave:
                if self.file_path == "":
                    self.save_as_file()
                    if self.file_path != "":
                        event.accept()
                elif self.file_path != "":
                    self.save_file()
                    event.accept()
            elif self.msgBox.clickedButton() == self.buttonDontSave:
                event.accept()
            elif self.msgBox.clickedButton() == self.buttonCancel:
                event.ignore()


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    MyNotepad = Ui_MyNotepad()
    ui = Ui_MyNotepad()
    ui.setupUi(MyNotepad)
    MyNotepad.show()
    sys.exit(app.exec_())
Sergey
  • 1
  • 2
  • 1
    Please provide a [mre]. Also, remove those `connectSlotsByName` as they make no sense, if you want to call a function, call it. – musicamante May 05 '23 at 22:55
  • The guidelines say, "Copy the actual text from your code editor, paste it into the question, then format it as code." – Paul Cornelius May 06 '23 at 02:28
  • 1
    @Sergey Sorry but the code **must** be in the question, and it also must be **both** minimal *and* reproducible. This means that you do not have to put all your code, but provide an example, based on that code, that only includes the essential parts that allow us to reproduce the issue. Please read that link more carefully and understand the request, otherwise we'll not be able to answer you. – musicamante May 06 '23 at 04:13
  • @musicamante, I tried to insert the code into the question, but the limit on the number of characters does not allow me to do this. Even if I put only the code created using Qt Designer, there will be more characters than the restriction allows – Sergey May 06 '23 at 21:07
  • 1
    @Sergey If that happens, it's because your example is not minimal. Code in a question should rarely go over 150-200 lines, or should at least have a reasonable ratio with the rest of the text. And I sincerely doubt that you need more than 100 lines to show the minimum code specifically related to what you're asking. You probably don't need the UI file at all, as a simple widget including the editor will certainly suffice for this purpose, and for that you need no more than 10-15 lines. – musicamante May 06 '23 at 22:08
  • @musicamante, Thanks for the help with the minimal example. I removed unnecessary functions and buttons from the code to leave only the functionality that describes my problem. – Sergey May 07 '23 at 11:18
  • It's a typo. You're creating two instances of `Ui_MyNotepad` and using the wrong one. Change to `MyNotepad.setupUi(MyNotepad)` or, just add `self.setupUi(self)` in the `__init__`. Note that this is caused by the fact that you're trying to edit or merge the output of a `pyuic` generated file, which is a bad practice. When you want to implement the code logic of the UI, you create a separate python script as your main file and import the pyuic file **as it is**, as explained in the official guidelines about [using Designer](//www.riverbankcomputing.com/static/Docs/PyQt5/designer.html). – musicamante May 07 '23 at 18:08
  • Note that there are other important issues (also caused by the above problem), like accessing the global `MyNotepad` reference (which, instead, would become `self` if you properly follow the guidelines above), or using `connectSlotsByName` as said above. Finally, Qt already supports the `*` indicator for modified files, read more carefully the documentation about the [`windowTitle`](https://doc.qt.io/qt-5/qwidget.html#windowTitle-prop) property, and also use the `windowModified` of the widget linked to the `isModified` property of the text document of the editor. – musicamante May 07 '23 at 18:12
  • @musicamante, Thank you very much. I am very grateful to you for your help. I'll take your recommendations into account. – Sergey May 08 '23 at 12:39

0 Answers0