QT 学习笔记(九)

简介: QT 学习笔记(九)

文章目录


一、事件的接收和忽略

1. 准备工作

2. 接收和忽略

二、event() 函数

1. 简介

2. 实例演示

3. 总结

三、事件过滤器

四、总结(细看)

1. 知识点汇总

2. QT 的事件处理

五、事件、事件的接收和忽略、event() 函数和事件过滤器代码

1. 主窗口头文件 mywidget.h

2. 主窗口源文件 mywidget.cpp

3. 标签头文件 mylabel.h

4. 标签源文件 mylabel.cpp

5. 按钮头文件 mybutton.h

6. 按钮源文件 mybutton.cpp


由于每次代码都是在原有程序上修改,因此除了新建项目,不然一般会在学完后统一展示代码。

提示:具体项目创建流程和注意事项见QT 学习笔记(一)

提示:具体项目准备工作和细节讲解见QT 学习笔记(二)



一、事件的接收和忽略

1. 准备工作


在 ui 界面新建一个按钮,在项目文件当中新建一个 mybutton 的 C++ 类。

将 ui 界面的按钮提升为 mybutton 。自定义控件提升详见QT 学习笔记(七)。

在 主窗口源文件 mywidget.cpp 当中对按钮进行操作,对基本功能进行测验。

注意:要在项目文件 day8.pro 当中添加 CONFIG +=C++11 才可以使 connect 功能正常实现。

872dce80d35444c2a86253ccfcbc8f81.png

当我们点击按钮时,会在输出窗口输出按钮被按下,如下图所示:

f436c2c0f58a421b9d4c66dae1036c07.png


2. 接收和忽略


  • 在上一步的检测操作完成后,在按钮源文件 mybutton.cpp 当中,进行事件的接收和忽略操作。
  • 通过 if 语句,对鼠标左键按下在输出窗口输出按下的是左键,对其他鼠标按键不做处理,调用原函数。
  • 在上述操作当中,鼠标左键对应的就是事件的接收其他按键对应的就是事件的忽略


fab054413d14475a8fde901fac1f62d4.png


在这里有一个问题,当我们按下鼠标左键时,会输出按下的是左键,但当我们按下其他按键时,并没有调用原函数,即输出按钮被按下。

这是因为当我们按下鼠标左键的时候,事件的信号被 qDebug() 输出语句给拦截了,不会再向后传递,无法走到 else 对应的内容。

因此,在我们编写代码时,事件的忽略不要编写实现代码即可。


  • 事件的接收,就不会往下传递。
  • 事件的忽略,事件会继续往下传递。
  • 我们可以把 QT 的事件传递看成链状:如果子类没有处理这个事件,就会继续向其父组件传递。
  • QT 的事件对象有两个函数:accept() 和 ignore() 。


  • accept() 用来告诉 QT,这个类的事件处理函数想要处理这个事件;
  • 如果一个事件处理函数调用了一个事件对象的 accept() 函数,这个事件就不会被继续传播给其父组件。


ignore() 用来告诉 QT,这个类的事件处理函数不想要处理这个事件;

如果调用了事件的 ignore() 函数,QT 会从其父组件中寻找另外的接受者。在事件处理函数中,可以使 isAccepted() 来查询这个事件是不是已经被接收了。

我们在主窗口 myWidget.cpp 中,重写鼠标点击事件,每当鼠标按下的时候,输出 ++++++++++,同时,将按钮窗口 mybutton.cpp 当中的 e->ignore() 注释掉,得到如下现象:

a0723ea980cb4b61a765d88340b91bb0.png


这样操作并不会输出 ++++++++++。此时,我们取消按钮窗口 mybutton.cpp 当中的 e->ignore() 的注释,得到下面的现象:

6a8948ca405f4b979fe143a4ebd1a690.png


当我们点击按钮的时候,会同时输出按下的是左键和 ++++++++++,当我们点击按钮外的界面时,只会输出 ++++++++++。

这是因为没有接收到信号,所以没有触发那里的 e->ignore()。而这里的 e->ignore() 可以让这个信号再次传递下去,不过不是传递给 mybutton,而是 父组件 mywidget。

这个 e->ignore() 主要使用在关闭项目当中,也就是窗口右上角大家熟知的红色关闭按钮,在此简单实现一下(只展示实现现象,具体代码会在总体代码当中展示)。

在我们实际关闭窗口的时候,往往会弹出一句 确定关闭吗? 这种类似的提示。

为了实现这样的功能,需要包含头文件 QMessageBox ,使用 question() 函数。question() 函数中的第一个参数是指定父对象,第二个参数是标题,第三个参数是提示内容,第四个参数不写的话默认有两个按钮:yes 和 no 。

为实现功能,只需要一个简单 if 语句即可,如果点击 yes ,则处理关闭窗口事件,接收事件,事件就不会再往下传递;如果点击 no ,则忽略事件,事件继续给父组件传递。

具体现象如下图所示:

d8c8f6985ff049f0931e03d88e0c92c7.png


二、event() 函数

1. 简介


事件对象创建完毕后,QT 将这个事件对象传递给 QObject 的 event() 函数。event() 函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler)。

因此,event() 函数主要用于 事件的分发。所以,如果你希望在事件分发之前做一些操作,就可以重写这个 event() 函数了。

例如,我们希望在一个 QWidget 组件中监听 tab 键的按下,那么就可以继承 QWidget,并重写它的 event() 函数,来达到这个目的。

事件分发如下图所示。

78f6e2179dfa4ee790f3c8752470f472.png


与其他的事件返回值不同的是,event() 事件的返回值是 bool 类型。

如果传入的事件已被识别并且处理,则需要返回 true,否则返回 false。如果返回值是 true,那么 QT 会认为这个事件已经处理完毕,不会再将这个事件发送给其它对象, 而是会继续处理事件队列中的下一事件。

在 event() 函数中,调用事件对象的 accept() 和 ignore() 函数是没有作用的,不会影响到事件的传播。

相当于在所有信号接收前进行一个检查,满足要求的停止工作,其他的继续原动作。


2. 实例演示


  • 在此以定时器为例,重写 event() 函数,使得定时器停止工作,其他部件保持原动作。代码和实现结果如下:
//重写event事件
bool myWidget::event(QEvent *e)
{
        if(e->type()==QEvent::Timer)
    {
        //干掉定时器
        //如果返回true,事件停止传播
        return true;
    }
    else
    {
        return QWidget::event(e);
    }
}


e2be07c749d54af4baa49e9b1de31317.png


  • 将其恢复正常需要一个强制类型转换,只需在上述代码当中添加如下代码即可:
QTimerEvent *env = static_cast<QTimerEvent *>(e);
        timerEvent(env);


正常现象如下图所示:

edcc6e16cf0d41a781afe63db163938f.png

  • 我们在此继续进行一个按键 B 的 event() 函数重写,只有当我们按下按键 B 的时候,事件才继续,其他的事件均终止。代码和实现结果如下:
else if(e->type()==QEvent::KeyPress)
    {
        //类型转换
        QKeyEvent *env = static_cast<QKeyEvent *>(e);
        if(env->key()==Qt::Key_B)
        {
            return QWidget::event(e);
        }
        return true;
    }


  • 我们在此继续进行一个按键 B 的 event() 函数重写,只有当我们按下按键 B 的时候,事件才继续,其他的事件均终止。代码和实现结果如下:
else if(e->type()==QEvent::KeyPress)
    {
        //类型转换
        QKeyEvent *env = static_cast<QKeyEvent *>(e);
        if(env->key()==Qt::Key_B)
        {
            return QWidget::event(e);
        }
        return true;
    }


429fae1c368644e08f30203d3aeea293.png


3. 总结

QTimerEvent() 和 QKeyEvent() 这样的函数,就是前面所说的事件处理器 event handler。

event() 函数中实际是通过事件处理器来响应一个具体的事件。这相当于 event() 函数将具体事件的处理“委托”给具体的事件处理器。而这些事件处理器是 protected virtual 的,因此,我们重写了某一个事件处理器,即可让 QT 调用我们自己实现的版本。

event() 是一个集中处理不同类型的事件的地方。如果你想重写一大堆事件处理器,就可以重写这个 event() 函数,通过 QEvent::type() 判断不同的事件。由于重写 event() 函数需要十分小心注意父类的同名函数的调用,一不留神就可能出现问题。

因此,一般只重写事件处理器(当然,也必须记得是不是应该调用父类的同名处理器)。这其实也表明了 event() 函数的另外一个作用:屏蔽掉某些不需要的事件处理器。


三、事件过滤器


很多时候,对象需要查看、甚至要拦截发送到另外对象的事件。例如,对话框可能想要拦截按键事件,不让别的组件接收到;或者要修改回车键的默认处理。

通过前文,我们已经知道,QT 创建了 QEvent 事件对象之后,会调用 QObject 的 event() 函数处理事件的分发。显然,可以在 event() 函数中实现拦截的操作。由于 event() 函数是 protected 的,因此,需要继承已有类。如果组件很多,就需要重写很多个 event() 函数。这当然相当麻烦,更不用说重写 event() 函数还得小心一堆问题。

因此,我们采用 QT 提供的另外一种机制来达到这一目的:事件过滤器。

8443b59616f44d548d95723d624103bc.png



  • QObject 有一个 eventFilter() 函数,用于建立事件过滤器。函数原型如下:
virtual bool QObject::eventFilter ( QObject * watched, QEvent * event );


事件过滤器,可以理解成一种过滤代码。事件过滤器会检查接收到的事件。如果这个事件是我们感兴趣的类型,就进行我们自己的处理;如果不是,就继续转发。这个函数返回一个 bool 类型,如果你想将参数 event 过滤出来,比如,不想让它继续转发,就返回 true,否则返回 false。

事件过滤器的调用时间是目标对象(也就是参数里面的 watched 对象)接收到事件对象之前。也就是说,如果你在事件过滤器中停止了某个事件,那么,watched 对象以及以后所有的事件过滤器根本不会知道这么一个事件。

在实现事件过滤器之前,我们要在 ui 界面选取一个合适的标签,在这里选择的是 label_2 。 在选择完成后进行操作安装过滤器,该函数的参数是由哪个函数的父对象进行处理。

//安装过滤器
ui->label_2->installEventFilter(this);
  • 通过事件过滤器,将 label_2 ,转换为鼠标移动,并实时显示出对应的 x 和 y 坐标,其他事件保持不变。(鼠标按下和释放的整体流程与鼠标移动是相同的,按钮的操作方法和标签也是相同的,具体代码在汇总时展示)

63ee3f7ca6af4edaa04d9bc866be09f8.png


注意:事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。



四、总结(细看)

1. 知识点汇总


QT 的事件是整个 QT 框架的核心机制之一。涉及到的函数众多,而处理方法也很多。

QT 中有很多种事件:鼠标事件、键盘事件、大小改变的事件、位置移动的事件等等。如何处理这些事件,实际有两种选择:

(1) 所有事件对应一个事件处理函数,在这个事件处理函数中用一个很大的分支语句进行选择。

(2) 每一种事件对应一个事件处理函数。QT 就是使用的这么一种机制:mouseEvent() ,keyPressEvent() 等。

QT 具有这么多种事件处理函数,肯定有一个地方对其进行分发,否则,QT 怎么知道哪一种事件调用哪一个事件处理函数呢?这个分发的函数,就是 event()。显然,当 QMouseEvent 产生之后, event() 函数将其分发给 mouseEvent() 事件处理器进行处理。


event() 函数会有两个问题:


1) event() 函数是一个 protected 的函数,这意味着我们要想重写 event() ,必须继承一个已有的类。试想,我的程序根本不想要鼠标事件,程序中所有组件都不允许处理鼠标事件,是不是我得继承所有组件,一 一重写其 event() 函数?protected 函数带来的另外一个问题是,如果我基于第三方库进行开发,而对方没有提供源代码,只有一个链接库,其它都是封装好的。我怎么去继承这种库中的组件呢?

(2) event() 函数的确有一定的控制,不过有时候需求更严格一些:我希望那些组件根本看不到这种事件。event() 函数虽然可以拦截,但其实也是接收到了 QMouseEvent 对象。我连让它收都收不到。这样做的好处是,模拟一种系统根本没有那个事件的效果,所以其它组件根本不会收到这个事件,也就无需修改自己的事件处理函数。

这两个问题是 event() 函数无法处理的。于是,QT 提供了另外一种解决方案:事件过滤器。事件过滤器给我们一种能力,让我们能够完全移除某种事件。事件过滤器可以安装到任意 QObject 类型上面,并且可以安装多个。如果要实现全局的事件过滤器,则可以安装到 QApplication 或者 QCoreApplication 上面。这里需要注意的是,如果使用 installEventFilter() 函数给一个对象安装事件过滤器,那么该事件过滤器只对该对象有效,只有这个对象的事件需要先传递给事件过滤器的 eventFilter() 函数进行过滤,其它对象不受影响。如果给 QApplication 对象安装事件过滤器,那么该过滤器对程序中的每一个对象都有效,任何对象的事件都是先传给 eventFilter() 函数。

事件过滤器可以解决刚刚我们提出的 event() 函数的两点不足:

(1) 事件过滤器不是 protected 的,因此我们可以向任何 QObject 子类安装事件过滤器;

(2) 事件过滤器在目标对象接收到事件之前进行处理,如果我们将事件过滤掉,目标对象根本不会见到这个事件。


2. QT 的事件处理


QT 的事件处理,实际上是有五个层次:

(1) 重写 paintEvent() 、mousePressEvent() 等事件处理函数。这是最普通、最简单的形式,同时功能也最简单。

(2) 重写 event() 函数。event() 函数是所有对象的事件入口,QObject 和 QWidget 中的实现,默认是把事件传递给特定的事件处理函数。

(3) 在特定对象上面安装事件过滤器。该过滤器仅过滤该对象接收到的事件。

(4) 在 QCoreApplication::instance() 上面安装事件过滤器。该过滤器将过滤所有对象的所有事件,因此和 notify() 函数一样强大,但是它更灵活,因为可以安装多个过滤器。全局的事件过滤器可以看到 disabled 组件上面发出的鼠标事件。全局过滤器有一个问题:只能用在主线程。

(5) 重写 QCoreApplication::notify() 函数。这是最强大的,和全局事件过滤器一样提供完全控制,并且不受线程的限制。但是全局范围内只能有一个被使用(因为 QCoreApplication 是单例的)。


五、事件、事件的接收和忽略、event() 函数和事件过滤器代码

1. 主窗口头文件 mywidget.h

#ifndef MYWIDGET_H
#define MYWIDGET_H
#include <QWidget>
namespace Ui {
class myWidget;
}
class myWidget : public QWidget
{
    Q_OBJECT
public:
    explicit myWidget(QWidget *parent = nullptr);
    ~myWidget();
protected:
    //键盘按下事件
    void keyPressEvent(QKeyEvent *);
    //计时器事件
    void timerEvent(QTimerEvent *);
    //重写鼠标点击事件
    void mousePressEvent(QMouseEvent *);
    //关闭事件
    void closeEvent(QCloseEvent *);
    //重写event事件
    bool event(QEvent *);
    //事件过滤器
    bool eventFilter(QObject *,QEvent *);
private:
    Ui::myWidget *ui;
    int timeid;
    int timeid2;
};
#endif // MYWIDGET_H

2. 主窗口源文件 mywidget.cpp

#include "mywidget.h"
#include "ui_mywidget.h"
#include <QDebug>
#include <QKeyEvent>
#include <QCloseEvent>
#include <QMessageBox>
#include <QEvent>
myWidget::myWidget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::myWidget)
{
    ui->setupUi(this);
    //启动计时器
    timeid = this->startTimer(1000);  //毫秒为单位。每隔1s触发一次定时器
    timeid2 = this->startTimer(500);  //毫秒为单位。每隔0.5s触发一次定时器
    connect(ui->pushButton,&mybutton::clicked,
            [=]()
    {
        qDebug()<<"按钮被按下";
    }
            );
    //安装过滤器
    ui->label_2->installEventFilter(this);
    //设置鼠标追踪
    ui->label_2->setMouseTracking(tr);
}
myWidget::~myWidget()
{
    delete ui;
}
//键盘按下事件
void myWidget::keyPressEvent(QKeyEvent *e)
{
    qDebug()<<(char)e->key();
    if(e->key()==Qt::Key_A)
    {
        qDebug()<<"Qt::Key_A";
    }
}
//计时器事件
void myWidget::timerEvent(QTimerEvent *e)
{
    if(e->timerId()==this->timeid)
    {
        static int sec = 0;
        ui->label->setText(
            QString("<center><h1>time out:%1</h1></center>").arg(sec++)
               );
        //停止计时器
        /*if(5==sec)
        {
            this->killTimer(this->timeid);
        }    */
    }
    else if (e->timerId()==this->timeid2)
    {
        static int sec = 0;
        ui->label_2->setText(
            QString("<center><h1>time out:%1</h1></center>").arg(sec++)
               );
        //停止计时器
        /*if(5==sec)
        {
            this->killTimer(this->timeid);
        }    */
    }
}
//重写鼠标点击事件
void myWidget::mousePressEvent(QMouseEvent *e)
{
    qDebug()<<"++++++++++";
}
//重写鼠标点击事件
void myWidget::closeEvent(QCloseEvent *e)
{
    int ret = QMessageBox::question(this,"question","是否需要关闭窗口");
    //question 第一个参数是指定父对象,第二个参数是标题,
    //第三个参数是提示内容,第四个参数不写的话默认有两个按钮:yes和no。
    if(ret==QMessageBox::Yes)
    {
        //关闭窗口
        //处理关闭窗口事件,接收事件,事件就不会再往下传递
        e->accept();
    }
    else
    {
        //不关闭窗口
        //忽略事件,事件继续给父组件传递
        e->ignore();
    }
}
//重写event事件
bool myWidget::event(QEvent *e)
{
    if(e->type()==QEvent::Timer)
    {
        //干掉定时器
        //如果返回true,事件停止传播
        //QTimerEvent *e
        //QTimerEvent *env = static_cast<QTimerEvent *>(e);
        //timerEvent(env);
        return true;
    }
    else if(e->type()==QEvent::KeyPress)
    {
        //类型转换
        QKeyEvent *env = static_cast<QKeyEvent *>(e);
        if(env->key()==Qt::Key_B)
        {
            return QWidget::event(e);
        }
        return true;
    }
    else
    {
        return QWidget::event(e);
    }
}
//事件过滤器
bool myWidget::eventFilter(QObject *obj,QEvent *e)
{
    if(obj==ui->label_2)
    {
        //类型转换
        QMouseEvent *env=static_cast<QMouseEvent *>(e);
        //判断事件
        if(e->type()==QEvent::MouseMove)
        {
            ui->label_2->setText
                    (QString("mouse move:(%1,%2)").arg(env->x()).arg(env->y()));
            return true;
        }
        else
        {
            return QWidget::eventFilter(obj,e);
        }
    }
    else
    {
        return QWidget::eventFilter(obj,e);
    }
}

3. 标签头文件 mylabel.h

#ifndef MYLABEL_H
#define MYLABEL_H
#include <QLabel>
class mylabel : public QLabel
{
    Q_OBJECT
public:
    explicit mylabel(QWidget *parent = nullptr);
protected:
    //鼠标点击事件
    void mousePressEvent(QMouseEvent *ev);
    //鼠标释放事件
    void mouseReleaseEvent(QMouseEvent *ev);
    //鼠标移动事件
    void mouseMoveEvent(QMouseEvent *ev);
    //进入窗口区域
    void enterEvent(QEvent *);
    //离开窗口区域
    void leaveEvent(QEvent *);
signals:
public slots:
};
#endif // MYLABEL_H


4. 标签源文件 mylabel.cpp

#include "mylabel.h"
#include <QMouseEvent>
mylabel::mylabel(QWidget *parent) : QLabel(parent)
{
}
//鼠标点击事件
void mylabel::mousePressEvent(QMouseEvent *ev)
{
    int i=ev->x();
    int j=ev->y();
    //sprinf 字符串格式化命令
    /*
     * QString str = QString("abc %1 ^_^ %2").arg(123).arg("mike");
     * str = abc 123 ^_^ mike
    */
    QString text = QString("<center><h1>mouse press:(%1,%2)</h1></center>")
            .arg(i).arg(j);
    // center 居中,h1 一级标题
    this->setText(text);
}
//鼠标释放事件
void mylabel::mouseReleaseEvent(QMouseEvent *ev)
{
    QString text = QString("<center><h1>mouse release:(%1,%2)</h1></center>")
            .arg(ev->x()).arg(ev->y());
    this->setText(text);
}
//鼠标移动事件
void mylabel::mouseMoveEvent(QMouseEvent *ev)
{
    QString text = QString("<center><h1>mouse move:(%1,%2)</h1></center>")
            .arg(ev->x()).arg(ev->y());
    //this->setText(text);
}
//进入窗口区域
void mylabel::enterEvent(QEvent *e)
{
    QString text = QString("<center><h1>mouse enter</h1></center>");
    this->setText(text);
}
//离开窗口区域
void mylabel::leaveEvent(QEvent *e)
{
    QString text = QString("<center><h1>mouse leave</h1></center>");
    this->setText(text);
}



5. 按钮头文件 mybutton.h

#ifndef MYBUTTON_H
#define MYBUTTON_H
#include <QPushButton>
class mybutton : public QPushButton
{
    Q_OBJECT
public:
    explicit mybutton(QWidget *parent = nullptr);
protected:
    void mousePressEvent(QMouseEvent *e);
signals:
public slots:
};
#endif // MYBUTTON_H



6. 按钮源文件 mybutton.cpp

#include "mybutton.h"
#include <QMouseEvent>
#include <QDebug>
mybutton::mybutton(QWidget *parent) : QPushButton(parent)
{
}
void mybutton::mousePressEvent(QMouseEvent *e)
{
    if(e->button() == Qt::LeftButton)
    {
        //如果是左键按下
        qDebug()<<"按下的是左键";
        //事件接收后,就会往下传递
        e->ignore();
        //忽略,事件继续往下传递,事件传递给了父组件,不是给父类(基类)
    }
    else
    {
        //不做处理
        QPushButton::mousePressEvent(e);
        //事件的忽略,事件继续往下传递
    }
}



























相关文章
|
1月前
【Qt 学习笔记】Qt窗口 | 标准对话框 | 消息对话框QMessageBox
【Qt 学习笔记】Qt窗口 | 标准对话框 | 消息对话框QMessageBox
206 4
【Qt 学习笔记】Qt窗口 | 标准对话框 | 消息对话框QMessageBox
|
1月前
|
开发者
【Qt 学习笔记】Qt系统相关 | Qt事件 | 事件的介绍及基本概念
【Qt 学习笔记】Qt系统相关 | Qt事件 | 事件的介绍及基本概念
120 4
|
1月前
【Qt 学习笔记】Qt窗口 | 标准对话框 | 文件对话框QFileDialog
【Qt 学习笔记】Qt窗口 | 标准对话框 | 文件对话框QFileDialog
268 4
|
1月前
|
数据安全/隐私保护
【Qt 学习笔记】Qt窗口 | 对话框 | 模态与非模态对话框的创建
【Qt 学习笔记】Qt窗口 | 对话框 | 模态与非模态对话框的创建
159 4
|
1月前
|
搜索推荐 C++
【Qt 学习笔记】Qt窗口 | 对话框 | 创建自定义对话框
【Qt 学习笔记】Qt窗口 | 对话框 | 创建自定义对话框
32 4
|
1月前
|
API UED
【Qt 学习笔记】Qt窗口 | 状态栏 | QStatusBar的使用及说明
【Qt 学习笔记】Qt窗口 | 状态栏 | QStatusBar的使用及说明
127 4
|
1月前
【Qt 学习笔记】Qt窗口 | 标准对话框 | 输入对话框QInputDialog
【Qt 学习笔记】Qt窗口 | 标准对话框 | 输入对话框QInputDialog
72 3
|
1月前
|
数据可视化
【Qt 学习笔记】Qt窗口 | 标准对话框 | 字体对话框QFontDialog
【Qt 学习笔记】Qt窗口 | 标准对话框 | 字体对话框QFontDialog
40 3
|
1月前
【Qt 学习笔记】Qt窗口 | 标准对话框 | 颜色对话框QColorDialog
【Qt 学习笔记】Qt窗口 | 标准对话框 | 颜色对话框QColorDialog
169 3
|
1月前
【Qt 学习笔记】Qt窗口 | 对话框 | Qt对话框的分类及介绍
【Qt 学习笔记】Qt窗口 | 对话框 | Qt对话框的分类及介绍
69 3