Friday, March 6, 2009

My PyQt Scribbles (Python and Qt) #5: Event-Handling Mechanism (QEvent: moveEvent() and closeEvent())

In this installment of My PyQt Scribbles we will have a look to the event-handling mechanism.

As I already mentioned it's my custom to explore new concepts through examples. What I call ‘useful examples' are the implementation of concepts in plausibly real situation. This means that I won't just write an example by detaching a particular mechanism from the whole app. I will not write something which is too abstract or that does something that replicate an useless function. This scribbles of mine are mostly an exercise and a tool I exploit to study a subject I like. As far as I can I will implement all the concepts into the building of a specific application which will slowly grow and increase its functionality as I explore further subjects.

Today we will make our application more fashionable by replicating the transparency feature you can see on KDE (with composition manager enabled), Mac OSX and Vista. Basically, our app will get transparent when we move it around the screen. We will also implement a dialog widget which pops up when we try to close the application to ask us if we're sure to quit or not.

Events are members of the QEvent class and a sort of low level objects that represent things that have happened either in the outside environment or inside the application. They're processed at different level by the Qt application.
Events can be divided in three different types:

Spontaneous events à they originate outside the application (window system) and are queued and processed by the event loop
Posted events à they are generated by Qt or the application and are queued and processed by the event loop
Sent events à they are generated by Qt or the application but instead to be managed by the loop they're sent directly to the target object

When an event happens, Qt generates a specific object to represent that specific event. This event is then shipped to the children/parents chain for handling and, if no Object handles it in some way the event object is just discarded.
A corollary to this is that we usually don't need to send (sendEvent() )or post (postEvent() ) events explicitly because most events are generated automatically by Qt or by the window system when they happen. If you want to "manually" send an event, you can call high-level functions to do this (i.e. update() and repaint()).




Events in Qt can be processed on five different levels but the first two are the most common and needed. The last three are rarely used so you don't need to go deeper in the concept related to them unless you're a Qt guru (in this case you're probably wasting your time reading this ‘scrible').

• Reimplementing a specific event handler.
QObject and QWidget provide many specific event handlers for different types of events (for example, moveEvent() for handling the movements).
• Reimplementing QObject::event().
The event() function is the ancestor of all the object's events. The default behaviour in QObject and QWidget simply forward the events to the specific event handlers.
• Installing an event filter on a QObject.
An event filter is an object that receives another object's events before they reach the intended target.
• Installing an event filter on qApp. Exceptionally, an event filter on qApp monitors all events sent to all objects in the application.
• Reimplementing QApplication::notify(). Qt's event loop and sendEvent() call this function to dispatch events. By reimplementing it, you get to see events before anybody else.

As I said before some type of events can be propagated to the children/parents chain. In this situation, if a target object doesn't process the event, Qt passes the event to other receivers until reaching the top-level parent object.

This is a brief explanation about what events are and how they work. If you find it confusing just try to grab the basic concepts by studying the code below. One approach I find very useful is not just to copy and paste the code as it is but try to modify it in your own way. You can maybe try to reimplement the basic concept to explore different events or handling them in a different way.
If you want to increase your knowledge you can check the following links and read the articles.
http://doc.trolltech.com/4.4/eventsandfilters.html
http://doc.trolltech.com/qq/qq11-events.html

So lets go on with the example.
As usual only the new parts are commented.

from PyQt4.QtCore import *
from PyQt4.QtGui import *

class WizAndChipsCal(QWidget):

        def __init__(self, parent = None):
                QWidget.__init__(self)
                self.setWindowTitle("Wiz and Chips Calendar")
                self.setWindowIcon(QIcon("C:/Python26/PyQt/Icon/date.png"))
                self.setToolTip("Hello to this Wiz and Chips fancy calendar!")
                self.title = (""\
                              + "Wiz and Chips Pushable Calendar!"\
                              + "")
                self.Label = QLabel(self.title)
                self.Label.setAlignment(Qt.AlignCenter | Qt.AlignJustify)

                self.calendar = QCalendarWidget()
                self.calendar.setGridVisible(1)
                self.calendar.setMinimumHeight(180)
                self.calendar.setMinimumWidth(110)

                self.check1 = QCheckBox("check1")
                self.check2 = QCheckBox("check2")
                self.TextBox = QTextEdit("type something here")
                self.TextBox.setMaximumHeight(50)

                self.dateLabel = QLabel("Date:")
                self.dateLabel.setMaximumWidth(80)
                CurrDate = QDate.currentDate()
                self.date = QDateEdit(CurrDate)
                self.date.setMaximumWidth(80)

                self.CloseButton = QPushButton("&Quit")
                self.CloseButton.setToolTip(""\
                                            + "Press here to Quit"\
                                            + "")
                self.CloseButton.setMaximumSize(50, 25)

                self.infobox = QGroupBox("Info Box")
                self.infobox.setCheckable(1)
                self.infobox.setChecked(0)

                dateLayout = QHBoxLayout()
                dateLayout.addWidget(self.dateLabel)
                dateLayout.addWidget(self.date)
                dateLayout.addSpacing(170)

                GBoxLayout = QVBoxLayout(self.infobox)
                GBoxLayout.setSpacing(1)

                GBoxLayout.addLayout(dateLayout)
                GBoxLayout.addWidget(self.check1)
                GBoxLayout.addWidget(self.check2)
                GBoxLayout.addWidget(self.TextBox)

                Layout = QGridLayout()
                Layout.addWidget(self.Label, 0, 0)
                Layout.addWidget(self.calendar, 1, 0)
                Layout.addWidget(self.CloseButton, 4, 0)
                Layout.addWidget(self.infobox, 3, 0)
                self.setLayout(Layout)

                self.connect(self.CloseButton,
                             SIGNAL("pressed()"),
                             self.close)
        def moveEvent(self, event):
                self.setWindowOpacity(0.7)
                QTimer.singleShot(50, self.opac)
        def opac(self):
                self.setWindowOpacity(1)

        def closeEvent(self, event):

                self.CloseDialog = QMessageBox.question(self, "The application is being closed",
                                                        "Do you really want to exit?",
                                                        QMessageBox.Save|QMessageBox.Yes|QMessageBox.Discard,
                                                        QMessageBox.Discard)
                if self.CloseDialog == QMessageBox.Yes:
                        event.accept()
                elif self.CloseDialog == QMessageBox.Save or QMessageBox.Discard:
                        event.ignore()

app = QApplication(sys.argv)
main_window = WizAndChipsCal()
main_window.show()
app.exec_()

def moveEvent(self, event):
self.setWindowOpacity(0.7)
QTimer.singleShot(50, self.opac)

Everytime the widget is moved (this includes the first time it's painted on the screen) the function change the widget opacity to make it transparent.
moveEvent() event handler is implemented to receive widget move events which are passed in the event parameter. When the widget receives this event, it is already at the new position.
Once the movement is finished we call QTimer.singleShot which counts a certain number of milliseconds and then calls another function which sets the opacity to 1 thus making the widget opaque again. singleShot is needed to tell QTimer to count just one time. Otherwise the count is made at intervals

def opac(self):
self.setWindowOpacity(1)

setWindowOpacity(1) property holds the level of opacity for the window. The range is from 1.0 (completely opaque) to 0.0 (completely transparent).
Note: This feature is only available on Mac OS X, X11 platforms that support the Composite extension, and Windows 2000 and later.

def closeEvent(self, event):
                self.CloseDialog = QMessageBox.question(self, "The application is being closed",
                                                        "Do you really want to exit?",
                                                        QMessageBox.Save|QMessageBox.Yes|QMessageBox.Discard,
                                                        QMessageBox.Discard)

This event handler is called with the given event when Qt receives a window close request for a top-level widget from the window system.
We create a dialog widget by implementing QmessageBox.question(). This class displays a dialog widget with an informative text for the user and provides standard buttons to enable the user to interact. By default QMessageBox support preformatted (layout, icons etc..) message boxes for questions (QMessageBox.question()), information (QMessageBox.information()), warning (QMessageBox.warning()) and critical (QMessageBox.critical()).

The configuration of our question box is: QMessageBox.question ( QWidget * parent, QString=title, Qstring=text, StandardButtons=buttons, StandardButton=defaultButton)
Our default button must be picked from the buttons already used, we pick discard in order to make our exit procedure safer. Note: save button will now have the same behaviour of ‘discard'. We will reserve the function for later usage.
There are several standard buttons already specified by the API.
For more information about message boxes see http://doc.trolltech.com/4.5/qmessagebox.html

if self.CloseDialog == QMessageBox.Yes:
                        event.accept()
                elif self.CloseDialog == QMessageBox.Save or QMessageBox.Discard:
                        event.ignore()

By default, closeEvent eventt is accepted and the widget is closed. We instead reimplement this function to change the way the widget responds to the close request.
If the user press ‘Yes' we call accept() and the window is closed. If the button pressed is ‘Save' or ‘Discard' we call ignore() and the event is thrown away.




No comments:

Post a Comment