现在的位置: 首页 > 综合 > 正文

Qt C++ GUI

2014年01月31日 ⁄ 综合 ⁄ 共 14221字 ⁄ 字号 评论关闭

用户界面应用程序:

子类化QMainWindow:Spreadsheet例子

首先将MainWindow类定义为QMainWindow的子类。其包含自己的信号和槽,所以包含了Q_OBJECT宏

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

class QAction;
class QLabel;
class FindDialog;
class Spreadsheet;

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow();

protected:
    void closeEvent(QCloseEvent *event);

closeEvent()函数是QWidget类中的一个虚函数,当用户关闭的时候,自动调用。

void MainWindow::closeEvent(QCloseEvent *event)
{
    if (okToContinue()) {
        writeSettings();
        event->accept();
    } else {
        event->ignore();
    }
}

private slots:
    void newFile();
    void open();
    bool save();
    bool saveAs();
    void find();
    void goToCell();
    void sort();
    void about();
    void openRecentFile();
    void updateStatusBar();
    void spreadsheetModified();

private:
    void createActions();
    void createMenus();
    void createContextMenu();
    void createToolBars();
    void createStatusBar();
    void readSettings();
    void writeSettings();
    bool okToContinue();
    bool loadFile(const QString &fileName);
    bool saveFile(const QString &fileName);
    void setCurrentFile(const QString &fileName);
    void updateRecentFileActions();
    QString strippedName(const QString &fullFileName);

接下来看看MainWidow的构造函数

首先创建一个Spreadsheet的窗口部件并且把它设置为这个主窗口的中央窗口部件,占据主窗口的中央区域。Spreadsheet是QTableWidget的子类。

MainWindow::MainWindow()
{
    spreadsheet = new Spreadsheet;
    setCentralWidget(spreadsheet);

    createActions();
    createMenus();
    createContextMenu();
    createToolBars();
    createStatusBar();

    readSettings();

    findDialog = 0;

    setWindowIcon(QIcon(":/images/icon.png"));
    setCurrentFile("");
}

creatActions()等创建窗口的其余部分,readSetting()则是读取这个应用程序存储的一些设置。

Qt用过动作的概念简化有关菜单和工具栏的编程,在Qt中,创建菜单和工具栏包括以下步骤

1.创建并且设置动作

2.创建菜单并且把动作添加到菜单上

3.创建工具栏并且把动作添加到工具栏上

在此应用程序中,动作实在createActions()函数中创建的

void MainWindow::createActions()
{
    newAction = new QAction(tr("&New"), this);
    newAction->setIcon(QIcon(":/images/new.png"));
    newAction->setShortcut(QKeySequence::New);
    newAction->setStatusTip(tr("Create a new spreadsheet file"));
    connect(newAction, SIGNAL(triggered()), this, SLOT(newFile()));

    openAction = new QAction(tr("&Open..."), this);
    openAction->setIcon(QIcon(":/images/open.png"));
    openAction->setShortcut(QKeySequence::Open);
    openAction->setStatusTip(tr("Open an existing spreadsheet file"));
    connect(openAction, SIGNAL(triggered()), this, SLOT(open()));

动作New有一个加速键,一个主窗口,一个图标,一个快捷键以及一个状态提示。

而动作的triggered()信号连接到主窗口的私有槽,确保按下的时候可以调用。

在Qt中,菜单都是QMenu的实例。addMenu()函数可以用给定的文本创建一个QMenu窗口部件,并且会把它添加到菜单栏中。

void MainWindow::createMenus()
{
    fileMenu = menuBar()->addMenu(tr("&File"));
    fileMenu->addAction(newAction);
    fileMenu->addAction(openAction);
    fileMenu->addAction(saveAction);
    fileMenu->addAction(saveAsAction);
    separatorAction = fileMenu->addSeparator();
    for (int i = 0; i < MaxRecentFiles; ++i)
        fileMenu->addAction(recentFileActions[i]);
    fileMenu->addSeparator();
    fileMenu->addAction(exitAction);

menuBar()函数返回一个指向QMenuBar的指针,菜单栏会在第一次调用menuBar()函数的时候就创建出来。接下来把几个项加入菜单。

separatorAction = fileMenu->addSeparator();

表示插入一个间隔器,可以把上面关系紧密的项放一起。

使用for循环添加一些最初隐藏起来的动作。

如果在一个菜单下的项目中还有小的项目,则一样用addMenu()添加

editMenu = menuBar()->addMenu(tr("&Edit"));
    editMenu->addAction(cutAction);
    editMenu->addAction(copyAction);
    editMenu->addAction(pasteAction);
    editMenu->addAction(deleteAction);

    selectSubMenu = editMenu->addMenu(tr("&Select"));
    selectSubMenu->addAction(selectRowAction);
    selectSubMenu->addAction(selectColumnAction);
    selectSubMenu->addAction(selectAllAction);

    editMenu->addSeparator();
    editMenu->addAction(findAction);
    editMenu->addAction(goToCellAction);

任何Qt窗口部件都可以有一个与之相关联的QActions列表,提供上下文菜单,可以将所需要的动作添加到Spreadsheet窗口部件中,相当于鼠标右击或则是在按键上面按下一个与平台相关的案件时候,就可以激活这些上下文菜单。

void MainWindow::createContextMenu()
{
    spreadsheet->addAction(cutAction);
    spreadsheet->addAction(copyAction);
    spreadsheet->addAction(pasteAction);
    spreadsheet->setContextMenuPolicy(Qt::ActionsContextMenu);
}

创建工具栏与创建菜单栏的过程相似,我们下面创建的是file工具栏和edit工具栏。

接下来是调用createStatusBar()函数来设置状态栏

void MainWindow::createStatusBar()
{
    locationLabel = new QLabel(" W999 ");
    locationLabel->setAlignment(Qt::AlignHCenter);
    locationLabel->setMinimumSize(locationLabel->sizeHint());

    formulaLabel = new QLabel;
    formulaLabel->setIndent(3);

    statusBar()->addWidget(locationLabel);
    statusBar()->addWidget(formulaLabel, 1);

    connect(spreadsheet, SIGNAL(currentCellChanged(int, int, int, int)),
            this, SLOT(updateStatusBar()));
    connect(spreadsheet, SIGNAL(modified()),
            this, SLOT(spreadsheetModified()));

    updateStatusBar();
}

上面包含两个部分,一个是定位指示器,一个是单元格公式指示器。绑定的两个槽函数。

下面是实现File菜单

void MainWindow::newFile()
{
    if (okToContinue()) {
        spreadsheet->clear();
        setCurrentFile("");
    }
}

单击new按钮的时候,会调用newFile槽,接着执行okToContinue()判断是否有还没有被保存的信息。

bool MainWindow::okToContinue()
{
    if (isWindowModified()) {
        int r = QMessageBox::warning(this, tr("Spreadsheet"),
                        tr("The document has been modified.\n"
                           "Do you want to save your changes?"),
                        QMessageBox::Yes | QMessageBox::No
                        | QMessageBox::Cancel);
        if (r == QMessageBox::Yes) {
            return save();
        } else if (r == QMessageBox::Cancel) {
            return false;
        }
    }
    return true;
}

返回的是布尔值。首先检测windowModified属性的状态。若是true,则显示出一个消息框。QMessageBox提供的按钮会自动让其中的一个成为默认的确认按钮,一个默认的推出按钮。

warning()函数:

warning(parent,title,message,buttons);

QMessageBox还提供了information(),question()和critical()函数,他们都有各自特定的图标。

open()函数也是一样,

void MainWindow::open()
{
    if (okToContinue()) {
        QString fileName = QFileDialog::getOpenFileName(this,
                                   tr("Open Spreadsheet"), ".",
                                   tr("Spreadsheet files (*.sp)"));
        if (!fileName.isEmpty())
            loadFile(fileName);
    }
}

首先还是要处理没有保存的变化,然后使用getOpenFileName()静态函数从用户那里获取一个新的文件名,这个函数会弹出一个对话框,让用户选择一个文件,然后返回文件名。若用户单击了取消按钮,则返回空字符串。其中第三个参数表示它应当从哪一级目录开始的,这里为当前目录。第四个参数是文件过滤器,由一个描述文本和通配符组成。

loadFile()函数用以载入文件。

bool MainWindow::loadFile(const QString &fileName)
{
    if (!spreadsheet->readFile(fileName)) {
        statusBar()->showMessage(tr("Loading canceled"), 2000);
        return false;
    }

    setCurrentFile(fileName);
    statusBar()->showMessage(tr("File loaded"), 2000);
    return true;
}

使用readFile()函数从磁盘中读取文件,若载入成功,则会调用setCurrentFiel()函数来更新窗口的标题,否则通过消息窗通知客户。显示2S的信息。

下面是所有保存操作函数

bool MainWindow::save()
{
    if (curFile.isEmpty()) {
        return saveAs();
    } else {
        return saveFile(curFile);
    }
}

bool MainWindow::saveAs()
{
    QString fileName = QFileDialog::getSaveFileName(this,
                               tr("Save Spreadsheet"), ".",
                               tr("Spreadsheet files (*.sp)"));
    if (fileName.isEmpty())
        return false;

    return saveFile(fileName);
}

bool MainWindow::saveFile(const QString &fileName)
{
    if (!spreadsheet->writeFile(fileName)) {
        statusBar()->showMessage(tr("Saving canceled"), 2000);
        return false;
    }

    setCurrentFile(fileName);
    statusBar()->showMessage(tr("File saved"), 2000);
    return true;
}

最后是退出。

void MainWindow::closeEvent(QCloseEvent *event)
{
    if (okToContinue()) {
        writeSettings();
        event->accept();
    } else {
        event->ignore();
    }
}

接下来实现的是在Spreadsheet窗口和Find对话框的切换,Find对话框必须是非模态的,就是运行在应用程序中对于任何其他窗口都是独立的窗口。

void MainWindow::find()
{
    if (!findDialog) {		//若Find对话框没有存在过,创建它并且把它的信号和槽连接好。
        findDialog = new FindDialog(this);
        connect(findDialog, SIGNAL(findNext(const QString &,
                                            Qt::CaseSensitivity)),
                spreadsheet, SLOT(findNext(const QString &,
                                           Qt::CaseSensitivity)));
        connect(findDialog, SIGNAL(findPrevious(const QString &,
                                                Qt::CaseSensitivity)),
                spreadsheet, SLOT(findPrevious(const QString &,
                                               Qt::CaseSensitivity)));
    }

    findDialog->show();		//让一个隐藏的窗口变为可见的,位于最上方的以及是激活的。
    findDialog->raise();		//若已经可见了则调用下面函数。
    findDialog->activateWindow();
}

Go to Cell 是一个模态的窗口,就是在弹出后可以阻塞应用程序的窗口,直到关闭。

第五章 

创建自定义的窗口部件(通过对一个已经存在的Qt窗口部件进行子类话或者直接对QWidget进行子类化来创建)

通过对一个十六进制微调框来说明:一般QSpinBox只支持十进制的整数,但通过子类化方法,可以让它接受并且显示十六进制数。

#ifndef HEXSPINBOX_H
#define HEXSPINBOX_H

#include <QSpinBox>

class QRegExpValidator;

class HexSpinBox : public QSpinBox	//继承QSpinBox
{
    Q_OBJECT

public:
    HexSpinBox(QWidget *parent = 0);

protected:
    QValidator::State validate(QString &text, int &pos) const;
    int valueFromText(const QString &text) const;
    QString textFromValue(int value) const;

private:
    QRegExpValidator *validator;
};

#endif

#include <QtGui>

#include "hexspinbox.h"

HexSpinBox::HexSpinBox(QWidget *parent)
    : QSpinBox(parent)
{
    setRange(0, 255);	//设置默认的范围,0x00到0xff
    validator = new QRegExpValidator(QRegExp("[0-9A-Fa-f]{1,8}"), this);
	/*用户可以通过单击微调框的上下键头或者在它的行编辑器中输入数值来修改微调框的当前值。
	在后面一种情况中,当我们要严格控制用户输入的数据是合法的十六进制数字的时候,我们使用这条语句
	它可以接受1到8个字符,所有的字符都必须是0到9,a到f,以及A到F。
	*/
}

QValidator::State HexSpinBox::validate(QString &text, int &pos) const//检测用户输入文本的合法性,返回三种结果,有效,部分有效无效,无效。
{
    return validator->validate(text, pos);//直接用validate返回结果就好。有自适应能力
}

int HexSpinBox::valueFromText(const QString &text) const//执行从字符串到整数值的逆向转换。
{
    bool ok;
    return text.toInt(&ok, 16);//将当前文本转换成整数值,若字符串不是有效的十六进制数,将OK设为false,并且返回0,在这里,因为输入的一定是十六进制数。
}

QString HexSpinBox::textFromValue(int value) const	//将一个整数值转换成一个字符串
{
    return QString::number(value, 16).toUpper();//用QString::number将该值转换成小写格式的字符串,再调用toUpper()转换成大写格式。
}

自定义其他Qt的窗口部件也是遵循相同的模式,选择合适的Qt窗口部件,对它进行子类化,并重新实现一些虚函数来改变它的行为。

而如果我们只是想对一个已经存在的窗口部件的外观进行自定义设置,那么只要对其中应用一个样式表或者重新实现一种自定义风格即可,而不必对其进行子类化。

程序启动画面

使用的是QSplashScreen类,通常程序启动画面的代码会放在main()函数中,位于exec()调用之前。

例如

QSplashScreen *splash = new QSplashScreen;

splash->setPixmap(QPixmap("......"));

splash->show();

......

参见C++ GUI QT4

事件处理:

1.键盘事件:

通过重新实现啊keyPressEvent()和keyReleaseEvent()就可以处理键盘事件了,通常只需要实现,keyPressEvent()就可以了,通常释放重要性的键值是Ctrl等,而这些键的状态可以用QKeyEvent::modifiers()检测出来。

void Plotter::keyPressEvent(QKeyEvent *event)
{
    switch (event->key()) {
    case Qt::Key_Plus:
        zoomIn();
        break;
    case Qt::Key_Minus:
        zoomOut();
        break;
    case Qt::Key_Left:
        zoomStack[curZoom].scroll(-1, 0);
        refreshPixmap();
        break;
    case Qt::Key_Right:
        zoomStack[curZoom].scroll(+1, 0);
        refreshPixmap();
        break;
    case Qt::Key_Down:
        zoomStack[curZoom].scroll(0, -1);
        refreshPixmap();
        break;
    case Qt::Key_Up:
        zoomStack[curZoom].scroll(0, +1);
        refreshPixmap();
        break;
    default:
        QWidget::keyPressEvent(event);
    }
}

Tab键和BackTab(Shift+Tab)键是两种特殊的情况,在窗口部件调用keyPressEvent()之前,QWidget::event()会先处理它们,但在有些窗口部件中,我们更愿意让Tab键起到缩进文本的作用,所以重新实现实现后的event()应该如下所示

bool CodeEditor::event(QEvent *event)

{

if(event->type()==QEvent::KeyPress)

{

QKeyEvent *keyEvent=static_cast<QKeyEvent*>(event);//如果该事件是按键事件,就把QEvent对象强制转换成QKeyevent并检查按下的是哪个键。

if(keyEvent->key()==Qt::Key_Tab)//如果按下的是Tab键,做一些处理并返回true,告诉Qt处理完了。

{

insertAtCurrentPosition('/t');

return true;

}

return QWidget::event(event);//如果不是,将这个事件传递给父窗口部件来处理。

}

}

2.定时器事件:允许应用程序在一定的时间间隔执行事件处理,只是用来实现光标的闪烁以及其他动画的播放,或者只是简单的刷新。

ticket.cpp

#include <QtGui>

#include "ticker.h"

Ticker::Ticker(QWidget *parent)
    : QWidget(parent)
{
    offset = 0;		//用来绘制文本的X坐标值就取自于这个offset
    myTimerId = 0;	//定时器的ID通常非零,用表示定时器还没有启动。
}

void Ticker::setText(const QString &newText)	//用来设置要显示的文本
{
    myText = newText;
    update();		//强制执行一个重绘操作。
    updateGeometry();//通知对Ticker窗口部件负责的任意布局管理器,该窗口部件大小发生改变
}

QSize Ticker::sizeHint() const	//返回文本所需要的空间大小,并以此作为窗口部件的理想尺寸
{
    return fontMetrics().size(0, text());//fontmetrics()返回一个QFontMetrics对象,查询并获得与这个窗口部件字体相关的信息。
}

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;
    }
}

void Ticker::showEvent(QShowEvent * /* event */)		//启动定时器
{
    myTimerId = startTimer(30);//startTimer()函数返回一个数字,以后能用该数字识别该定时器。
}	//大约每30毫秒Qt会产生一个定时器时间,时间精度取决于操作系统

void Ticker::timerEvent(QTimerEvent *event)		//系统每隔一段时间,都会调用timerEvent函数
{
    if (event->timerId() == myTimerId) {
        ++offset;
        if (offset >= fontMetrics().width(text()))//滚动完一圈
            offset = 0;
        scroll(-1, 0);//把窗口部件的内容像左移动一个像素
    } else {
        QWidget::timerEvent(event);
    }
}

void Ticker::hideEvent(QHideEvent * /* event */)
{
    killTimer(myTimerId);	//停止该定时器
    myTimerId = 0;
}

ticket.h

#ifndef TICKER_H
#define TICKER_H

#include <QWidget>

class Ticker : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(QString text READ text WRITE setText)

public:
    Ticker(QWidget *parent = 0);

    void setText(const QString &newText);
    QString text() const { return myText; }
    QSize sizeHint() const;

protected:
    void paintEvent(QPaintEvent *event);
    void timerEvent(QTimerEvent *event);
    void showEvent(QShowEvent *event);
    void hideEvent(QHideEvent *event);

private:
    QString myText;
    int offset;
    int myTimerId;
};

#endif

main.cpp

#include <QApplication>

#include "ticker.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    Ticker ticker;
    ticker.setWindowTitle(QObject::tr("Ticker"));
    ticker.setText(QObject::tr("How long it lasted was impossible to "
                               "say ++ "));
    ticker.show();
    return app.exec();
}

安装事件过滤器

Qt的事件模型一个强大的功能是一个QObject对象能够监视发送其他QObject对象的事件,在事件到达之前对其进行处理。

假设我们有一个CustomerInfoDialog控件,由一些QLineEdit控件组成。我们希望使用Space键得到下一个QLineEdit的输入焦点。一个最直接的方法是继承QLineEdit重写keyPressEvent()函数,当点击了Space键时,调用focusNextChild():
void MyLineEdit::keyPressEvent(QKeyEvent *event)
{
    if (event->key() == Qt::Key_Space) {
        focusNextChild();
    } else {
        QLineEdit::keyPressEvent(event);
    }
}
这个方法有一个最大的缺点:如果我们在窗体中使用了很多不同类型的控件(QComboBox,QSpinBox等等),我们也要继承这些控件,重写它们的keyPressEvent()。一个更好的解决方法是让CustomerInfoDialog监视其子控件的键盘事件,在监视代码处实现以上功能。这就是事件过滤的方法。实现一个事件过滤包括两个步骤:
1.      在目标对象上调用installEventFilter(),注册监视对象。
2.      在监视对象的eventFilter()函数中处理目标对象的事件。
注册监视对象的位置是在CustomerInfoDialog的构造函数中:
CustomerInfoDialog::CustomerInfoDialog(QWidget *parent)
    : QDialog(parent)
{
    ...
    firstNameEdit->installEventFilter(this);
    lastNameEdit->installEventFilter(this);
    cityEdit->installEventFilter(this);
    phoneNumberEdit->installEventFilter(this);
}
事件过滤器注册后,发送到firstNameEdit,lastNameEdit,cityEdit,phoneNumberEdit控件的事件首先到达CustomerInfoDialog::eventFilter()函数,然后在到达最终的目的地。
下面是eventFilter()函数的代码:
bool CustomerInfoDialog::eventFilter(QObject *target, QEvent *event)
{
    if (target == firstNameEdit || target == lastNameEdit
            || target == cityEdit || target == phoneNumberEdit) {
        if (event->type() == QEvent::KeyPress) {
            QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
            if (keyEvent->key() == Qt::Key_Space) {
                focusNextChild();
                return true;
            }
        }
    }
    return QDialog::eventFilter(target, event);
}
首先,我们看是目标控件是否为QLineEdit,如果事件为键盘事件,把QEvent转换为QKeyEvent,确定被敲击的键。如果为Space键,调用focusNextChild(),把焦点交给下一个控件,返回true通知Qt已经处理了这个事件,如果返回false,Qt将会把事件传递给目标控件,把一个空格字符插入到QLineEdit中。
如果目标控件不是QLineEdit,或者事件不是Space敲击事件,把控制权交给基类QDialog的eventFilter()。目标控件也可以是基类QDialog正在监视的控件。(在Qt4.1中,QDialog没有监视的控件,但是Qt的其他控件类,如QScrollArea,监视一些它们的子控件)
Qt的事件处理有5中级别:
1.      重写控件的事件处理函数:如重写keyPressEvent(),mousePressEvent()和paintEvent(),这是最常用的事件处理方法,我们已经看到过很多这样的例子了。
2.      重写QObject::event(),在事件到达事件处理函数时处理它。在需要改变Tab键的惯用法时这样做。也可以处理那些没有特定事件处理函数的比较少见的事件类型(例如,QEvent::HoverEnter)。我们重写event()时,必须要调用基类的event(),由基类处理我们不需要处理的那些情况。
3.      给QObject对象安装事件过滤器:对象用installEventFilter()后,所有达到目标控件的事件都首先到达监视对象的eventFilter()函数。如果一个对象有多个事件过滤器,过滤器按顺序激活,先到达最近安装的监视对象,最后到达最先安装的监视对象。
4.      给QApplication安装事件过滤器,如果qApp(唯一的QApplication对象)安装了事件过滤器,程序中所有对象的事件都要送到eventFilter()函数中。这个方法在调试的时候非常有用,在处理非活动状态控件的鼠标事件时这个方法也很常用。
5.      继承QApplication,重写notify()。Qt调用QApplication::nofity()来发送事件。重写这个函数是在其他事件过滤器处理事件前得到所有事件的唯一方法。通常事件过滤器是最有用的,因为在同一时间,可以有任意数量的事件过滤器,但是notify()函数只有一个。
许多事件类型,包括鼠标,键盘事件,是能够传播的。如果事件在到达目标对象的途中或者由目标对象处理掉,事件处理的过程会重新开始,不同的是这时的目标对象是原目标对象的父控件。这样从父控件再到父控件,知道有控件处理这个事件或者到达了最顶级的那个控件。
图7.2显示了一个键盘事件在一个对话框中从子控件到父控件的传播过程。当用户敲击一个键盘,时间首先发送到有焦点的控件上(这个例子中是QCheckBox)。如果QCheckBox没有处理这个事件,Qt把事件发送到QGroupBox中,如果仍然没有处理,则最后发送到QDialog中。
Figure 7.2. Event propagation in a dialog

抱歉!评论已关闭.