Extending PyQT4 text editor

Check out the new site at https://rkblog.dev.

No we will add two features to our editor. This will train our docs searching skills :nice:.

Disabled "Save" button

When there is no open file, or no changes the "Save" button should be disabled. In QTDesigner in the Property Editor we can set "enabled" attribute to "False" to disable the button.
pyqtii1_4
If Designer can do this so does PyQT4. Used textEdit widget has "textChanged()" signal so this part is easy. In the pushButton docs there is nothing about "enabled", but notice this line (before methods list):
Inherits QAbstractButton.
pushButton class inherits QAbstractButton and has its methods. When we go to QAbstractButton docs we wont see any method related to "enabled", but QAbstractButton inherits QWidget, and QWidget has setEnabled() method. So here is the start.py file:
# -*- coding: utf-8 -*-
import sys
from PyQt4 import QtCore, QtGui
from edytor import Ui_notatnik

class StartQT4(QtGui.QMainWindow):
	def __init__(self, parent=None):
		QtGui.QWidget.__init__(self, parent)
		self.ui = Ui_notatnik()
		self.ui.setupUi(self)
		QtCore.QObject.connect(self.ui.button_open,QtCore.SIGNAL("clicked()"), self.file_dialog)
		QtCore.QObject.connect(self.ui.button_save,QtCore.SIGNAL("clicked()"), self.file_save)
		QtCore.QObject.connect(self.ui.editor_window,QtCore.SIGNAL("textChanged()"), self.enable_save)
	def file_dialog(self):
		fd = QtGui.QFileDialog(self)
		self.filename = fd.getOpenFileName()
		from os.path import isfile
		if isfile(self.filename):
			import codecs
			s = codecs.open(self.filename,'r','utf-8').read()
			self.ui.editor_window.setPlainText(s)
			# inserting text emits textChanged() so we disable the button :)
			self.ui.button_save.setEnabled(False)
	def enable_save(self):
		self.ui.button_save.setEnabled(True)
	def file_save(self):
		from os.path import isfile
		if isfile(self.filename):
			import codecs
			s = codecs.open(self.filename,'w','utf-8')
			s.write(unicode(self.ui.editor_window.toPlainText()))
			s.close()
			self.ui.button_save.setEnabled(False)

if __name__ == "__main__":
	app = QtGui.QApplication(sys.argv)
	myapp = StartQT4()
	myapp.show()
	sys.exit(app.exec_())
I've added the slot with signal connection:
QtCore.QObject.connect(self.ui.editor_window,QtCore.SIGNAL("textChanged()"), self.enable_save)
def enable_save(self):
	self.ui.button_save.setEnabled(True)
In the file_dialog slot when we add text to the textEdit from file the "textChanged()" signal will be emitted so we have disable the "Save" button after it:
self.ui.editor_window.setPlainText(s)
# inserting text emits textChanged() so we disable the button :)
self.ui.button_save.setEnabled(False)
An the "Save" button will work as planned.

Save od Discard changes

When we want to open a file and we didn't saved changes to the current open file a message box should appear asking what to do about those changes - Save, Discard, Cancel. We will use QMessageBox. Go show it we need only:
message = QtGui.QMessageBox(self)
message.exec_()
pyqtii2_4
The window needs to be configured. We have to add buttons and some text. But how do we use it? We need to edit file_dialog method and if there are unsaved changes show the message. And how we will know if there are unsaved changes ? If the "Save" button is active (self.ui.button_save.isEnabled()) then we have unsaved changes. So here is start.py:
# -*- coding: utf-8 -*-
import sys
from PyQt4 import QtCore, QtGui
from edytor import Ui_notatnik

class StartQT4(QtGui.QMainWindow):
	def __init__(self, parent=None):
		QtGui.QWidget.__init__(self, parent)
		self.ui = Ui_notatnik()
		self.ui.setupUi(self)
		QtCore.QObject.connect(self.ui.button_open,QtCore.SIGNAL("clicked()"), self.file_dialog)
		QtCore.QObject.connect(self.ui.button_save,QtCore.SIGNAL("clicked()"), self.file_save)
		QtCore.QObject.connect(self.ui.editor_window,QtCore.SIGNAL("textChanged()"), self.enable_save)
	def file_dialog(self):
		response = False
		# buttons texts
		SAVE = 'Save'
		DISCARD = 'Discard'
		CANCEL = 'Cancel'
		# if we have changes then ask about them
		if self.ui.button_save.isEnabled() and self.filename:
			message = QtGui.QMessageBox(self)
			message.setText('What to do about unsaved changes ?')
			message.setWindowTitle('Notepad')
			message.setIcon(QtGui.QMessageBox.Question)
			message.addButton(SAVE, QtGui.QMessageBox.AcceptRole)
			message.addButton(DISCARD, QtGui.QMessageBox.DestructiveRole)
			message.addButton(CANCEL, QtGui.QMessageBox.RejectRole)
			message.setDetailedText('Unsaved changes in file: ' + str(self.filename))
			message.exec_()
			response = message.clickedButton().text()
			# save  file
			if response == SAVE:
				self.file_save()
				self.ui.button_save.setEnabled(False)
			# discard changes
			elif response == DISCARD:
				self.ui.button_save.setEnabled(False)
		# if we didn't cancelled show the file dialogue
		if response != CANCEL:
			fd = QtGui.QFileDialog(self)
			self.filename = fd.getOpenFileName()
			from os.path import isfile
			if isfile(self.filename):
				import codecs
				s = codecs.open(self.filename,'r','utf-8').read()
				self.ui.editor_window.setPlainText(s)
				self.ui.button_save.setEnabled(False)
	def enable_save(self):
		self.ui.button_save.setEnabled(True)
	def file_save(self):
		from os.path import isfile
		if isfile(self.filename):
			import codecs
			s = codecs.open(self.filename,'w','utf-8')
			s.write(unicode(self.ui.editor_window.toPlainText()))
			s.close()
			self.ui.button_save.setEnabled(False)

if __name__ == "__main__":
	app = QtGui.QApplication(sys.argv)
	myapp = StartQT4()
	myapp.show()
	sys.exit(app.exec_()))
The new part is:
response = False
# buttons texts
SAVE = 'Save'
DISCARD = 'Discard'
CANCEL = 'Cancel'
# if we have changes then ask about them
if self.ui.button_save.isEnabled() and self.filename:
	message = QtGui.QMessageBox(self)
	message.setText('What to do about unsaved changes ?')
	message.setWindowTitle('Notepad')
	message.setIcon(QtGui.QMessageBox.Question)
	message.addButton(SAVE, QtGui.QMessageBox.AcceptRole)
	message.addButton(DISCARD, QtGui.QMessageBox.DestructiveRole)
	message.addButton(CANCEL, QtGui.QMessageBox.RejectRole)
	message.setDetailedText('Unsaved changes in file: ' + str(self.filename))
	message.exec_()
	response = message.clickedButton().text()
	# save  file
	if response == SAVE:
		self.file_save()
		self.ui.button_save.setEnabled(False)
	# discard changes
	elif response == DISCARD:
		self.ui.button_save.setEnabled(False)
# if we didn't cancelled show the file dialogue
if response != CANCEL:
We create a QtGui.QMessageBox and then we set the message text (setText), window title (setWindowTitle), icon (setIcon, values are in the docs), and then we add 3 buttons ("Save", "Discard" i "Cancel"). As a second argument we add "role" of the button. Values are in the docs and they are responsible for the order of those buttons. setDetailedText sets detailed message, and after that we run the QMessageBox with exec_(). message.clickedButton() will return a pushButton object of the clicked button. To figure out which one was clicked we can compare buttons text. The Message Box looks like this:
pyqtii3_4

Download

Download sources
Note: to get English names on the buttons regenerate "edytor.py" class using "edytorEN.ui" and run the application using "startEN.py"
RkBlog

PyQt and GUI, 14 July 2008


Check out the new site at https://rkblog.dev.
Comment article
Comment article RkBlog main page Search RSS Contact