The static open() function pops up a file dialog through which the user can choose a file. If a file is chosen, openFile() is
called to create an Editor and to read in the file's contents.
Editor *Editor::openFile(const QString &fileName, QWidget *parent)
{
Editor *editor = new Editor(parent);
if (editor->readFile(fileName)) {
editor->setCurrentFile(fileName);
return editor;
} else {
delete editor;
return 0;
}
}
This static function begins by creating a new Editor widget, and then attempts to read in the specified file. If the read is
successful, the Editor is returned; otherwise, the user is informed of the problem (in readFile()), the editor is deleted, and a
null pointer is returned.
bool Editor::save()
{
if (isUntitled) {
return saveAs();
} else {
return saveFile(curFile);
}
}
The save() function uses the isUntitled variable to determine whether it should call saveFile() or saveAs().
void Editor::closeEvent(QCloseEvent *event)
{
if (okToContinue()) {
event->accept();
} else {
event->ignore();
}
}
The closeEvent() function is reimplemented to allow the user to save unsaved changes. The logic is coded in the
okToContinue() function, which pops up a message box that asks, "Do you want to save your changes?" If
okToContinue() returns true, we accept the close event; otherwise, we "ignore" it and leave the window unaffected by it.
void Editor::setCurrentFile(const QString &fileName)
{
curFile = fileName;
isUntitled = false;
action->setText(strippedName(curFile));
document()->setModified(false);
setWindowTitle(strippedName(curFile) + "[*]");
setWindowModified(false);
}
165
The setCurrentFile() function is called from openFile() and saveFile() to update the curFile and
isUntitled variables, to set the window title and action text, and to set the document's "modified" flag to false. Whenever
the user modifies the text in the editor, the underlying QTextDocument emits the contentsChanged() signal and sets its
internal "modified" flag to true.
QSize Editor::sizeHint() const
{
return QSize(72 * fontMetrics().width('x'),
25 * fontMetrics().lineSpacing());
}
Finally, the sizeHint() function returns a size based on the width of the letter 'x' and the height of a text line. QMdiArea uses
the size hint to give an initial size to the window.
MDI is one way of handling multiple documents simultaneously. On Mac OS X, the preferred approach is to use multiple
top-level windows. We covered this approach in the "Multiple Documents" section of Chapter 3.
166
7. Event Processing
The Tab and Backtab (Shift+Tab) keys are special cases. QWidget::event() handles them before it calls
keyPressEvent(), with the semantic of passing the focus to the next or previous widget in the focus chain. This behavior is
usually what we want, but in a CodeEditor widget, we might prefer to make Tab indent a line. The event()
reimplementation would then look like this:
167
If the event is a key press, we cast the QEvent object to a QKeyEvent and check which key was pressed. If the key is Tab, we
do some processing and return true to tell Qt that we have handled the event. If we returned false, Qt would propagate the
event to the parent widget.
A higher-level approach for implementing key bindings is to use a QAction. For example, if goToBeginningOfLine() and
goToBeginningOfDocument() are public slots in the CodeEditor widget, and the CodeEditor is used as the central
widget in a MainWindow class, we could add the key bindings with the following code:
MainWindow::MainWindow()
{
editor = new CodeEditor;
setCentralWidget(editor);
goToBeginningOfLineAction =
new QAction(tr("Go to Beginning of Line"), this);
goToBeginningOfLineAction->setShortcut(tr("Home"));
connect(goToBeginningOfLineAction, SIGNAL(activated()),
editor, SLOT(goToBeginningOfLine()));
goToBeginningOfDocumentAction =
new QAction(tr("Go to Beginning of Document"), this);
goToBeginningOfDocumentAction->setShortcut(tr("Ctrl+Home"));
connect(goToBeginningOfDocumentAction, SIGNAL(activated()),
editor, SLOT(goToBeginningOfDocument()));
...
}
This makes it easy to add the commands to a menu or a toolbar, as we saw in Chapter 3. If the commands don't appear in the user
interface, the QAction objects could be replaced with a QShortcut object, the class used internally by QAction to support
key bindings.
By default, key bindings set using QAction or QShortcut on a widget are enabled whenever the window that contains the
widget is active. This can be changed using QAction::setShortcutContext() or QShortcut::setContext().
Another common type of event is the timer event. While most other event types occur as a result of a user action, timer events
allow applications to perform processing at regular time intervals. Timer events can be used to implement blinking cursors and
other animations, or simply to refresh the display.
To demonstrate timer events, we will implement the Ticker widget shown in Figure 7.1. This widget shows a text banner that
scrolls left by one pixel every 30 milliseconds. If the widget is wider than the text, the text is repeated as often as necessary to fill
the entire width of the widget.
Figure 7.1. The Ticker widget
168
We reimplement four event handlers in Ticker, three of which we have not seen before: timerEvent(), showEvent(),
and hideEvent().
Now let's review the implementation:
#include <QtGui>
#include "ticker.h"
Ticker::Ticker(QWidget *parent)
: QWidget(parent)
{
offset = 0;
myTimerId = 0;
}
The constructor initializes the offset variable to 0. The x-coordinate at which the text is drawn is derived from the offset
value. Timer IDs are always non-zero, so we use 0 to indicate that no timer has been started.
void Ticker::setText(const QString &newText)
{
myText = newText;
update();
updateGeometry();
}
The setText() function sets the text to display. It calls update() to request a repaint and updateGeometry() to notify
any layout manager responsible for the Ticker widget about a size hint change.
QSize Ticker::sizeHint() const
{
return fontMetrics().size(0, text());
}
The sizeHint() function returns the space needed by the text as the widget's ideal size. QWidget::fontMetrics()
returns a QFontMetrics object that can be queried to obtain information relating to the widget's font. In this case, we ask for
the size required by the given text. (The first argument to QFontMetrics::size() is a flag that isn't needed for simple
strings, so we just pass 0.)
void Ticker::paintEvent(QPaintEvent * /* event */)
{
QPainter painter(this);
int textWidth = fontMetrics().width(text());
if (textWidth < 1)
return;
int x = -offset;
while (x < width()) {
painter.drawText(x, 0, textWidth, height(),
Qt::AlignLeft | Qt::AlignVCenter, text());
x += textWidth;
}
}
169
The paintEvent() function draws the text using QPainter::drawText(). It uses fontMetrics() to ascertain how
much horizontal space the text requires, and then draws the text as many times as necessary to fill the entire width of the widget,
taking offset into account.
void Ticker::showEvent(QShowEvent * /* event */)
{
myTimerId = startTimer(30);
}
The showEvent() function starts a timer. The call to QObject::startTimer() returns an ID number, which we can use
later to identify the timer. QObject supports multiple independent timers, each with its own time interval. After the call to
startTimer(), Qt will generate a timer event approximately every 30 milliseconds; the accuracy depends on the underlying
operating system.
We could have called startTimer() in the Ticker constructor, but we save some resources by having Qt generate timer
events only when the widget is actually visible.
void Ticker::timerEvent(QTimerEvent *event)
{
if (event->timerId() == myTimerId) {
++offset;
if (offset >= fontMetrics().width(text()))
offset = 0;
scroll(-1, 0);
} else {
QWidget::timerEvent(event);
}
}
The system calls the timerEvent() function at intervals. It increments offset by 1 to simulate movement, wrapping at the
width of the text. Then it scrolls the contents of the widget one pixel to the left using QWidget::scroll(). It would have
been sufficient to call update() instead of scroll(), but scroll() is more efficient because it simply moves the existing
pixels on-screen and generates a paint event only for the widget's newly revealed area (a 1-pixel-wide strip in this case).
If the timer event isn't for the timer we are interested in, we pass it on to the base class.
void Ticker::hideEvent(QHideEvent * /* event */)
{
killTimer(myTimerId);
myTimerId = 0;
}
170