RkBlog

Hardware, programming and astronomy tutorials and reviews.

Rozbudowa tekstowego edytora w PyQT4

Dodajemy obsługę zapisu i kontroli zmian w pliku tekstowym umożliwiając zapisanie niezapisanych zmian przy próbie np. zamknięcia aplikacji.

Teraz zajmiemy się dalszą rozbudową edytora. Zaczniemy od małych usprawnień, które przećwiczą nasze umiejętności wyszukiwania metod i sygnałów w dokumentacji.

Wyłączony przycisk "Zapisz"

Gdy nie ma otwartego pliki i gdy tekst nie uległ zmianie przycisk "Zapisz" powinien być wyłączony. W QTDesignerze przy edycji przycisku mamy atrybut "enabled" i ustawienie go na "False" wyłączy przycisk:
pyqtii1_4
A jak jest w QTDesignerze to znaczy że włączać i wyłączać przycisk możemy również z poziomu PyQT4. Od strony okna edytora widget textEdit ma sygnał "textChanged()", co załatwia sprawę sygnału. Natomiast w dokumentacji pushButton nie ma nic co dotyczy "enabled". Zwróć jednak uwagę na tekst przed listą metod:
Inherits QAbstractButton.
Klasa pushButton dziedziczy i ma jej metody. Klikamy w odnośnik do QAbstractButton. Ta klasa też nie ma metod związanych z włączaniem i wyłączaniem, ale dziedziczy QWidget do której przechodzimy. W dokumentacji QWidget znajdujemy metodę setEnabled(), co wyjaśnia sprawę sygnału i slotu. Oto kod 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):
		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_())
Dodałem połączenie sygnału ze slotem:
QtCore.QObject.connect(self.ui.editor_window,QtCore.SIGNAL("textChanged()"), self.enable_save)
def enable_save(self):
	self.ui.button_save.setEnabled(True)
Dodatkowo w slocie file_dialog po dodaniu do okna textEdit tekstu z pliku nastąpi włączenie przycisku "Zapisz", gdyż tekst "uległ" zmianie. Dlatego ponownie go wyłączamy:
self.ui.editor_window.setPlainText(s)
# inserting text emits textChanged() so we disable the button :)
self.ui.button_save.setEnabled(False)
I teraz przycisk zapisz włączy się tylko gdy tekst otwartego pliku ulegnie zmianie.

Zapisz lub Porzuć zmiany

W przypadku próby otwarcia pliku gdy edytor ma niezapisane zmiany w obecnie otwartym powinno pojawiać się okno pozwalające zapisać zmiany lub je porzucić. W tym celu wykorzystamy okienko QMessageBox. Samo wywołanie widżetu da puste okno:
message = QtGui.QMessageBox(self)
message.exec_()
pyqtii2_4
Okno trzeba odpowiednio skonfigurować stosując dostępne metody. Musimy dodać przyciski i jakiś tekst wyjaśniający o co chodzi. Pozostaje również kwestia wykorzystania okna. Otóż w metodzie file_dialog wywołującej okno wyboru pliku musimy w zależności od klikniętego przycisku w QMessageBox (zapisz, odrzuć zmiany, anuluj) podjąć odpowiednie czynności. Pytanie - skąd będziemy wiedzieć że są niezapisane zmiany w pliku ? Dzięki metodzie self.ui.button_save.isEnabled(), która zwróci True jeżeli przycisk "Zapisz" jest aktywny - są zmiany do zapisania. Bez zbędnego gadania zmodyfikowany 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
		# teksty przycisków
		SAVE = 'Zapisz'
		DISCARD = 'Porzuć'.decode('UTF-8')
		CANCEL = 'Anuluj'
		# jeżeli są zmiany to pokazujemy okno QMessageBox
		if self.ui.button_save.isEnabled() and self.filename:
			message = QtGui.QMessageBox(self)
			message.setText('Nie zapisano zmian w pliku')
			message.setWindowTitle('Notatnik')
			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('Nie zapisano zmian w pliku: ' + str(self.filename))
			message.exec_()
			response = message.clickedButton().text()
			# zapis pliku
			if response == SAVE:
				self.file_save()
				self.ui.button_save.setEnabled(False)
			# odrzucenie zmian
			elif response == DISCARD:
				self.ui.button_save.setEnabled(False)
		# jeżeli nie anulowaliśmy to pokaż okno dialogowe wyboru pliku
		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_())
Dodany fragment to:
response = False
# teksty przycisków
SAVE = 'Zapisz'
DISCARD = 'Porzuć'.decode('UTF-8')
CANCEL = 'Anuluj'
# jeżeli są zmiany to pokazujemy okno QMessageBox
if self.ui.button_save.isEnabled() and self.filename:
	message = QtGui.QMessageBox(self)
	message.setText('Nie zapisano zmian w pliku')
	message.setWindowTitle('Notatnik')
	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('Nie zapisano zmian w pliku: ' + str(self.filename))
	message.exec_()
	response = message.clickedButton().text()
	# zapis pliku
	if response == SAVE:
		self.file_save()
		self.ui.button_save.setEnabled(False)
	# odrzucenie zmian
	elif response == DISCARD:
		self.ui.button_save.setEnabled(False)
# jeżeli nie anulowaliśmy to pokaż okno dialogowe wyboru pliku
if response != CANCEL:
Tworzymy obiekt QtGui.QMessageBox a następnie ustawiamy wiadomość okna (setText), tytuł okna (setWindowTitle), ikonę (setIcon, wartości podane w dokumentacji), a następnie dodajemy trzy przyciski ("Zapisz", "Porzuć" i "Anuluj"). Jako drugi parametr podajemy "rolę" przycisku, co wpływa na ich rozmieszczenie (wartości podane w dokumentacji). Następnie dodajemy opis szczegółowy (setDetailedText) i wywołujemy okno za mocą exec_(). Następnie używam message.clickedButton() co zwraca obiekt klikniętego pushButton. By zorientować się, który to porównuję tekst przycisku z tymi użytymi do utworzenia wszystkich z nich. Edytor działa, a okno wygląda mniej więcej tak:
pyqtii3_4

Pobierz źródła

Pobierz źródła
RkBlog

PyQt, 14 July 2008,

Comment article