Qt笔记总结

Qt笔记总结


作者:hackett

微信公众号:加班猿


一、常用控件

按钮类

QPushButton

QtoolButton

QRadioButton

item

QListWidget

容器类

QStackWidget

QWidget

QFrame

编辑类

QComboBox

QLineEdit

QTextEdit

显示类

QLabel

setOpenExternalLinks()设置为true自动打开,false要打开链接需要捕捉linkActivated()信号

//显示普通文本字符串
QLable *label = new QLable;
label->setText(“Hello, World!”);
//显示HTML格式的字符串
QLabel * label = new QLabel(this);
label ->setText("Hello, World");
label ->setText("<h1><a href=\"https://www.baidu.com\">
百度一下</a></h1>");
label ->setOpenExternalLinks(true);
QLabel * label = new QLabel(this);
label ->setText("Hello, World");
label ->setText("<h1><a href=\"https://www.baidu.com\">
百度一下</a></h1>");
// label->setOpenExternalLinks(true);
connect(label, &QLabel::linkActivated, 
this, &MyWidget::slotOpenUrl);

//槽函数   
void MyWidget::slotOpenUrl(const QString &link)
{
    QDesktopServices::openUrl(QUrl(link));
}

QProcessBar

QMessageBox

二、信号与槽

信号和槽

信号槽,实际就是观察者模式。当某个事件发生之后,它就会发出一个信号(signal)将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。当信号发出时,被连接的槽函数会自动被回调。

1、connect(sender, signal, receiver, slot);
  • sender:发送信号的对象
  • signal:发送对象发出的信号
  • receiver:接收信号的对象
  • slot:接收对象在接收到信号之后所需要调用的函数

例如:

/* &b1: 信号发出者,指针类型
* &QPushButton::pressed:处理的信号   (&发送者的类名::信号名字)
* this: 信号接收者
* &MainWidget::close:槽函数,信号处理函数 (&接收的类名::槽函数名字)
*/
connect(&b1, &QPushButton::pressed, this, &MainWidget::close);

自定义信号槽

自定义信号槽需要注意的事项

  • 发送者和接收者都需要是QObject的子类(当然,槽函数是全局函数、Lambda 表达式等无需接收者的时候除外);

  • 使用 signals 标记信号函数,信号是一个函数声明,返回 void,不需要实现函数代码;

  • 槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响;

  • 使用 emit 在恰当的位置发送信号;

  • 使用QObject::connect()函数连接信号和槽。

  • 任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数

/* 自定义槽,普通函数的用法
* Qt5:任意的成员函数,普通全局函数,静态函数
* 槽函数需要和信号一致(参数,返回值)
* 由于信号都是没有返回值,所以,槽函数一定没有返回值
*/
connect(b2, &QPushButton::released, this, &MainWidget::mySlot);

Lambda表达式

C++11中的Lambda表达式用于定义并创建匿名的函数对象

基本构成:[函数对象参数](操作符重载函数参数)mutable或exception ->返回值{函数体}

[站外图片上传中...(image-fbbe9a-1639136635274)]

1、函数对象参数:

[ ] 标识一个Lambda的开始,这部分必须存在,不能省略

  • 空,没有任何函数对象参数

  • =,值传递方式(作用范围:所有可见的局部变量以及所在类的this)

  • &,引用传递方式(作用范围:所有可见的局部变量以及所在类的this)

  • this,函数体内可以可以使用Lambda所在类中的成员变量

  • a,把a按值进行传递(默认为const不可修改,可添加mutable修饰符修改)

  • &b,把b按引用进行传递

  • &,a,b,除a和b进行值传递,其他参数按引用进行传递

2、操作符重载函数参数:

标识重载的()操作符的参数,没有参数时,这部分可以省略。

3、可修改标示符:

mutable声明,这部分可以省略。按值传递函数对象参数时,加上mutable修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)。

4、错误抛出标示符:

exception声明,这部分也可以省略。exception声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用throw(int)

5、函数返回值:

->返回值类型,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。

6、函数体:

{},标识函数的实现,这部分不能省略,但函数体可以为空。

三、Qt窗口系统

1、坐标体系

以左上角为原点,X向右增加,Y向下增加。

[站外图片上传中...(image-e7a622-1639136635274)]

2、QWidget

所有窗口及窗口控件都是从QWidget直接或间接派生出来的。

  1. 对象模型

    • Qt创建对象的时候会提供一个Parent对象指针

      在创建QObject对象时,可以提供一个其父对象,我们创建的这个QObject对象会自动添加到其父对象的children()列表。当父对象析构的时候,这个列表中的所有对象也会被析构。(注意,这里的父对象并不是继承意义上的父类!)

    • QWidget是能够在屏幕上显示的一切组件的父类

      QWidget继承自QObject,因此也继承了这种对象树关系。一个孩子自动地成为父组件的一个子组件,也可以自己删除子对象,它们会自动从其父对象列表中删除。

    • 当一个QObject对象在堆上创建的时候,Qt 会同时为其创建一个对象树。不过,对象树中对象的顺序是没有定义的。这意味着,销毁这些对象的顺序也是未定义的

    • 任何对象树中的 QObject对象 delete 的时候,如果这个对象有 parent,则自动将其从 parent 的children()列表中删除;如果有孩子,则自动 delete 每一个孩子。Qt 保证没有QObject会被 delete 两次,这是由析构顺序决定的。

3、QMainWindow

QMainWindow是一个为用户提供主窗口程序的类,包含一个菜单栏(menu bar)、多个工具栏(tool bars)、多个锚接部件(dock widgets)、一个状态栏(status bar)及一个中心部件(central widget)

[站外图片上传中...(image-124fb4-1639136635274)]

1、菜单栏

  • 创建菜单栏,通过QMainWindow类的menubar() 函数获取主窗口菜单栏指针

    QMenuBar *    menuBar() const
    
  • 创建菜单,调用QMenu的成员函数addMenu() 来添加菜单

    QAction* addMenu(QMenu * menu)
    QMenu* addMenu(const QString & title)
    QMenu* addMenu(const QIcon & icon, const QString & title)
    
  • 创建菜单项,调用QMenu的成员函数addAction() 来添加菜单项

    QAction* activeAction() const
    QAction* addAction(const QString & text)
    QAction* addAction(const QIcon & icon, const QString & text)
    QAction* addAction(const QString & text, const QObject * receiver,
     const char * member, const QKeySequence & shortcut = 0)
    QAction* addAction(const QIcon & icon, const QString & text, 
    const QObject * receiver, const char * member, 
    const QKeySequence & shortcut = 0)
    

2、工具栏

  • 直接调用QMainWindow类的addToolBar() 函数获取主窗口的工具条对象,每增加一个工具条都需要调用一次该函数。

  • n 插入属于工具条的动作,即在工具条上添加操作。通过QToolBar类的addAction() 函数添加。

  • n 工具条是一个可移动的窗口,它的停靠区域由QToolBar的allowAreas决定,包括:

    /*
    Qt::LeftToolBarArea       //停靠在左侧
    Qt::RightToolBarArea      //停靠在右侧
    Qt::TopToolBarArea        //停靠在顶部
    Qt::BottomToolBarArea     //停靠在底部
    Qt::AllToolBarAreas       //以上四个位置都可停靠
    */
    
    /*使用setAllowedAreas()函数指定停靠区域*/
    setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea);
    /*使用setMoveable()函数设定工具栏的可移动性*/
    setMoveable(false);//工具条不可移动, 只能停靠在初始化的位置上
    
    

3、状态栏

  • 派生自QWidget类,使用方法与QWidget类似,QStatusBar类常用成员函数:

    //添加小部件
    void addWidget(QWidget * widget, int stretch = 0)
    //插入小部件
    int   insertWidget(int index, QWidget * widget, int stretch = 0)
    //删除小部件
    void removeWidget(QWidget * widget)
    

4、对话框QDialog

Qt 中使用QDialog类实现对话框。

对话框分为模态对话框和非模态对话框。

  • 模态对话框,就是会阻塞同一应用程序中其它窗口的输入。

    模态对话框很常见,比如“打开文件”功能。你可以尝试一下记事本的打开文件,当打开文件对话框出现时,我们是不能对除此对话框之外的窗口部分进行操作的。

  • 与此相反的是非模态对话框,例如查找对话框,我们可以在显示着查找对话框的同时,继续对记事本的内容进行编辑。

1、标准对话框

Qt 的内置对话框大致分为以下几类:

  • QColorDialog: 选择颜色;

  • QFileDialog: 选择文件或者目录;

  • QFontDialog: 选择字体;

  • QInputDialog: 允许用户输入一个值,并将其值返回;

  • QMessageBox: 模态对话框,用于显示信息、询问问题等;

  • QPageSetupDialog: 为打印机提供纸张相关的选项;

  • QPrintDialog: 打印机配置;

  • QPrintPreviewDialog:打印预览;

  • QProgressDialog: 显示操作过程。

2、自定义消息框

Qt 支持模态对话框和非模态对话框。

模态与非模态的实现:

  • 使用QDialog::exec()实现应用程序级别的模态对话框

  • 使用QDialog::open()实现窗口级别的模态对话框

  • 使用QDialog::show()实现非模态对话框。

3、模态对话框

下面的示例中,我们调用了exec()将对话框显示出来,因此这就是一个模态对话框。当对话框出现时,我们不能与主窗口进行任何交互,直到我们关闭了该对话框。

void MainWindow::open()
{
    QDialog dialog;
    dialog.setWindowTitle(tr("Hello, dialog!"));
    dialog.exec();
}

4、非模态对话框

下面我们试着将exec()修改为show(),看看非模态对话框:

void MainWindow::open()
{
    QDialog dialog(this);
    dialog.setWindowTitle(tr("Hello, dialog!"));
    dialog.show();
}

是不是事与愿违?对话框竟然一闪而过!这是因为,show()函数不会阻塞当前线程,对话框会显示出来,然后函数立即返回,代码继续执行。注意,dialog 是建立在栈上的,show()函数返回,MainWindow::open()函数结束,dialog 超出作用域被析构,因此对话框消失了。知道了原因就好改了,我们将 dialog 改成堆上建立,当然就没有这个问题了:

void MainWindow::open()
{
    QDialog *dialog = new QDialog;
    dialog->setWindowTitle(tr("Hello, dialog!"));
    dialog->show();
}

如果你足够细心,应该发现上面的代码是有问题的:dialog 存在内存泄露!dialog 使用 new 在堆上分配空间,却一直没有 delete。解决方案也很简单:将 MainWindow 的指针赋给 dialog 即可。还记得我们前面说过的 Qt 的对象系统吗?

不过,这样做有一个问题:如果我们的对话框不是在一个界面类中出现呢?由于QWidget的 parent 必须是QWidget指针,那就限制了我们不能将一个普通的 C++ 类指针传给 Qt 对话框。另外,如果对内存占用有严格限制的话,当我们将主窗口作为 parent 时,主窗口不关闭,对话框就不会被销毁,所以会一直占用内存。在这种情景下,我们可以设置 dialog 的WindowAttribute:

void MainWindow::open()
{
    QDialog *dialog = new QDialog;
    dialog->setAttribute(Qt::WA_DeleteOnClose);
    dialog->setWindowTitle(tr("Hello, dialog!"));
    dialog->show();
}

setAttribute()函数设置对话框关闭时,自动销毁对话框。

1、消息对话框

QMessageBox用于显示消息提示。我们一般会使用其提供的几个 static 函数:

  • 显示关于对话框。

    void about(QWidget * parent, const QString & title, const QString & text)
    

    这是一个最简单的对话框,其标题是 title,内容是 text,父窗口是 parent。对话框只有一个 OK 按钮。

  • 显示关于 Qt 对话框。该对话框用于显示有关 Qt 的信息。

    void aboutQt(QWidget * parent, const QString & title = QString())
    
  • 显示严重错误对话框。

    StandardButton critical(QWidget * parent, 
    const QString & title, 
    const QString & text, 
    StandardButtons buttons = Ok, 
    StandardButton defaultButton = NoButton)
    

    这个对话框将显示一个红色的错误符号。我们可以通过 buttons 参数指明其显示的按钮。默认情况下只有一个 Ok 按钮,我们可以使用StandardButtons类型指定多种按钮。

  • 与QMessageBox::critical()类似,不同之处在于这个对话框提供一个普通信息图标。

    StandardButton information(QWidget * parent, 
    const QString & title, 
    const QString & text, 
    StandardButtons buttons = Ok, 
    StandardButton defaultButton = NoButton)
    
  • 与QMessageBox::critical()类似,不同之处在于这个对话框提供一个问号图标,并且其显示的按钮是“是”和“否”。

    StandardButton question(QWidget * parent,
    const QString & title, 
    const QString & text, 
    StandardButtons buttons = StandardButtons( Yes | No ), 
    StandardButton defaultButton = NoButton)
    
  • 与QMessageBox::critical()类似,不同之处在于这个对话框提供一个黄色叹号图标。

    StandardButton warning(QWidget * parent, 
    const QString & title, 
    const QString & text, 
    StandardButtons buttons = Ok, 
    StandardButton defaultButton = NoButton)
    

2、标准文件对话框

QFileDialog,也就是文件对话框。

openAction = new QAction(QIcon(":/images/file-open"),
 tr("&Open..."), this);
openAction->setShortcuts(QKeySequence::Open);
openAction->setStatusTip(tr("Open an existing file"));

saveAction = new QAction(QIcon(":/images/file-save"), 
tr("&Save..."), this);
saveAction->setShortcuts(QKeySequence::Save);
saveAction->setStatusTip(tr("Save a new file"));

QMenu *file = menuBar()->addMenu(tr("&File"));
file->addAction(openAction);
file->addAction(saveAction);

QToolBar *toolBar = addToolBar(tr("&File"));
toolBar->addAction(openAction);
toolBar->addAction(saveAction);

textEdit = new QTextEdit(this);
setCentralWidget(textEdit);

使用connect()函数,为这两个QAction对象添加响应的动作:

connect(openAction, &QAction::triggered, 
this, &MainWindow::openFile);
connect(saveAction, &QAction::triggered, 
this, &MainWindow::saveFile);

下面是最主要的openFile()和saveFile()这两个函数的代码:

//打开文件
void MainWindow::openFile()
{
    QString path = QFileDialog::getOpenFileName(this,
               tr("Open File"), ".", tr("Text Files(*.txt)"));
    if(!path.isEmpty()) 
{
        QFile file(path);
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) 
{
            QMessageBox::warning(this, tr("Read File"),
                         tr("Cannot open file:\n%1").arg(path));
            return;
        }
        QTextStream in(&file);
        textEdit->setText(in.readAll());
        file.close();
    } 
else 
{
        QMessageBox::warning(this, tr("Path"),
                             tr("You did not select any file."));
     }
}

//保存文件
void MainWindow::saveFile()
{
    QString path = QFileDialog::getSaveFileName(this,
               tr("Open File"), ".", tr("Text Files(*.txt)"));
    if(!path.isEmpty()) 
{
        QFile file(path);
        if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) 
{
            QMessageBox::warning(this, tr("Write File"),
                         tr("Cannot open file:\n%1").arg(path));
            return;
        }
        QTextStream out(&file);
        out << textEdit->toPlainText();
        file.close();
    } 
else 
{
        QMessageBox::warning(this, tr("Path"),
                             tr("You did not select any file."));
    }
}

QFileDialog::getOpenFileName()来获取需要打开的文件的路径。这个函数原型如下:

QString getOpenFileName(QWidget * parent = 0,
                        const QString & caption = QString(),
                        const QString & dir = QString(),
                        const QString & filter = QString(),
                        QString * selectedFilter = 0,
                        Options options = 0)

六个参数分别是:

  • parent:父窗口

  • caption:对话框标题

  • dir:对话框打开时的默认目录

    “.” 代表程序运行目录

    “/” 代表当前盘符的根目录(特指 Windows 平台;Linux 平台当然就是根目录),这个参数也可以是平台相关的,比如“C:\”等

  • filter:过滤器。

    我们使用文件对话框可以浏览很多类型的文件,但是,很多时候我们仅希望打开特定类型的文件

  • selectedFilter:默认选择的过滤器

  • options:对话框的一些参数设定

    比如只显示文件夹等等,它的取值是enum QFileDialog::Option,每个选项可以使用 | 运算组合起来

四、布局管理器

Qt提供了两种组件定位机制:绝对定位和布局定位。

绝对定位

绝对定位就是一种最原始的定位方法:给出这个组件的坐标和长宽值。

如果用户改变了窗口大小,比如点击最大化按钮或者使用鼠标拖动窗口边缘,采用绝对定位的组件是不会有任何响应的。

布局定位

只要把组件放入某一种布局,布局由专门的布局管理器进行管理。当需要调整大小或者位置的时候,Qt 使用对应的布局管理器进行调整。

Qt 提供的布局中以下三种是我们最常用的:

  1. QHBoxLayout:按照水平方向从左到右布局;
  2. QVBoxLayout:按照竖直方向从上到下布局;
  3. QGridLayout:在一个网格中进行布局,类似于 HTML 的 table;

自定义控件

UI的控件和自定义控件的父类(基类)要一样

选中UI控件 -> 提升

五、消息机制和事件

事件

Qt 中所有事件类都继承于QEvent

event()函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(event handler)

在所有组件的父类QWidget中,定义了很多事件处理的回调函数,如:

  • keyPressEvent()

  • keyReleaseEvent()

  • mouseDoubleClickEvent()

  • mouseMoveEvent()

  • mousePressEvent()

  • mouseReleaseEvent() 等。

这些函数都是 protected virtual 的,也就是说我们可以在子类中重新实现这些函数。下面来看一个例子:

class EventLabel : public QLabel
{
protected:
    void mouseMoveEvent(QMouseEvent *event);
    void mousePressEvent(QMouseEvent *event);
    void mouseReleaseEvent(QMouseEvent *event);
};
 
void EventLabel::mouseMoveEvent(QMouseEvent *event)
{
this->setText(QString("<center><h1>Move: (%1, %2)
</h1></center>").arg(QString::number(event->x()),
            QString::number(event->y())));
}
 
void EventLabel::mousePressEvent(QMouseEvent *event)
{
    this->setText(QString("<center><h1>Press:(%1, %2)
</h1></center>").arg(QString::number(event->x()),
                QString::number(event->y())));
}
 
void EventLabel::mouseReleaseEvent(QMouseEvent *event)
{
    QString msg;
    msg.sprintf("<center><h1>Release: (%d, %d)</h1></center>",
                event->x(), event->y());
    this->setText(msg);
}
 
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
 
    EventLabel *label = new EventLabel;
    label->setWindowTitle("MouseEvent Demo");
    label->resize(300, 200);
    label->show();
 
    return a.exec();
}

EventLabel继承了QLabel,重写了mousePressEvent()、mouseMoveEvent()和MouseReleaseEvent()三个函数。在鼠标按下(press)、鼠标移动(move)和鼠标释放(release)的时候,把当前鼠标的坐标值显示在这个Label上面。

运行上面的代码,当我们点击了一下鼠标之后,label 上将显示鼠标当前坐标值。

为什么要点击鼠标之后才能在mouseMoveEvent()函数中显示鼠标坐标值?

这是因为QWidget中有一个mouseTracking属性,该属性用于设置是否追踪鼠标。只有鼠标被追踪时,mouseMoveEvent()才会发出。如果mouseTracking是 false(默认即是),组件在至少一次鼠标点击之后,才能够被追踪,也就是能够发出mouseMoveEvent()事件。如果mouseTracking为 true,则mouseMoveEvent()直接可以被发出。

在构造函数里面设置label->setMouseTracking(true);即可

event()

event()函数主要用于事件的分发

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

bool CustomWidget::event(QEvent *e)
{
    if (e->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
        if (keyEvent->key() == Qt::Key_Tab) {
            qDebug() << "You press tab.";
            return true;
        }
    }
    return QWidget::event(e);
}

CustomWidget是一个普通的QWidget子类。我们重写了它的event()函数,这个函数有一个QEvent对象作为参数,也就是需要转发的事件对象。函数返回值是 bool 类型:

  • 如果传入的事件已被识别并且处理,则需要返回 true,否则返回 false。如果返回值是 true,那么 Qt 会认为这个事件已经处理完毕,不会再将这个事件发送给其它对象,而是会继续处理事件队列中的下一事件
  • 在event()函数中,调用事件对象的accept()和ignore()函数是没有作用的,不会影响到事件的传播

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

事件过滤器

有时候,对象需要查看、甚至要拦截发送到另外对象的事件。

QObject有一个eventFilter()函数,用于建立事件过滤器。函数原型如下:

virtual bool QObject::eventFilter ( QObject * watched, QEvent * event );

这个函数返回一个 bool 类型,如果你想将参数 event 过滤出来,比如,不想让它继续转发,就返回 true,否则返回 false。事件过滤器的调用时间是目标对象(也就是参数里面的watched对象)接收到事件对象之前。也就是说,如果你在事件过滤器中停止了某个事件,那么,watched对象以及以后所有的事件过滤器根本不会知道这么一个事件。

上面QWidget组件中event()函数监听 tab 键的按下修改为使用事件过滤器的版本

bool FilterObject::eventFilter(QObject *object, QEvent *event)
{
    if (object == target && event->type() == QEvent::KeyPress) 
{
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
        if (keyEvent->key() == Qt::Key_Tab) {
            qDebug() << "You press tab.";
            return true;
        } else {
            return false;
        }
    }
    return false;
}

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

总结

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

  1. 重写paintEvent()、mousePressEvent()等事件处理函数。这是最普通、最简单的形式,同时功能也最简单。
  2. 重写event()函数。event()函数是所有对象的事件入口,QObject和QWidget中的实现,默认是把事件传递给特定的事件处理函数。
  3. 重写event()函数。event()函数是所有对象的事件入口,QObject和QWidget中的实现,默认是把事件传递给特定的事件处理函数。
  4. 在QCoreApplication::instance()上面安装事件过滤器。该过滤器将过滤所有对象的所有事件,因此和notify()函数一样强大,但是它更灵活,因为可以安装多个过滤器。全局的事件过滤器可以看到 disabled 组件上面发出的鼠标事件。全局过滤器有一个问题:只能用在主线程。
  5. 重写QCoreApplication::notify()函数。这是最强大的,和全局事件过滤器一样提供完全控制,并且不受线程的限制。但是全局范围内只能有一个被使用(因为QCoreApplication是单例的)。

六、绘图和绘图设备

QPainter

QPainter用来执行绘制的操作;QPaintDevice是一个二维空间的抽象,QPaintEngine提供了画笔(QPainter)在不同的设备上进行绘制的统一的接口

[站外图片上传中...(image-2a03c1-1639136635274)]

上面的示意图告诉我们,<font color = red size = 4>Qt 的绘图系统实际上是,使用QPainter在QPainterDevice上进行绘制,它们之间使用QPaintEngine进行通讯(也就是翻译QPainter的指令)。</font>

下面我们通过一个实例来介绍QPainter的使用:

class PaintedWidget : public QWidget
{
    Q_OBJECT
public:
    PaintedWidget(QWidget *parent = 0);
protected:
    void paintEvent(QPaintEvent *);
}

注意我们重写了QWidget的paintEvent()函数。接下来就是PaintedWidget的源代码:

PaintedWidget::PaintedWidget(QWidget *parent) :
    QWidget(parent)
{
    resize(800, 600);
    setWindowTitle(tr("Paint Demo"));
}

void PaintedWidget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.drawLine(80, 100, 650, 500);
    painter.setPen(Qt::red);
    painter.drawRect(10, 10, 100, 400);
    painter.setPen(QPen(Qt::green, 5));
    painter.setBrush(Qt::blue);
    painter.drawEllipse(50, 150, 400, 200);
}

构造函数中,我们仅仅设置了窗口的大小和标题。而paintEvent()函数则是绘制的代码。

绘图设备

绘图设备是指继承QPainterDevice的子类。

QBitmap是QPixmap的一个子类,它的色深限定为1,可以使用 QPixmap的isQBitmap()函数来确定这个QPixmap是不是一个QBitmap。

QPixmap:和平台无关,针对屏幕进行了优化了,不能对图片进行修改

QImage:和平台无关,在线程中绘图,可以对图片进行优化

QPicture:保存绘图的状态(二进制文件)

1、QPixmap -> QImage

QPixmap a;

a.toImage();

2、QImage -> QPixmap

QImage b;

QPixmap::fromImage(b);

3、QPainter

QPainter p;

QPicture pic;

p.begin(&pic);

//绘图动作

p.end();

pic.save("路径");

4、加载图片

QPicture temp;

temp.load("路径");

不规则窗口

1、给窗口画一张背景图

2、去表框

3、设定属性(背景透明)

4、移动坐标是相对于屏幕而言

七、文件系统

I/O 设备的类图(Qt5):
[站外图片上传中...(image-c50e1d-1639136635274)]

  • QIODevice:所有 I/O 设备类的父类,提供了字节块读写的通用操作以及基本接口;
  • QFileDevice:Qt5新增加的类,提供了有关文件操作的通用实现。
  • QFlie:访问本地文件或者嵌入资源;
  • QTemporaryFile:创建和访问本地文件系统的临时文件;
  • QBuffer:读写QbyteArray, 内存文件;
  • QProcess:运行外部程序,处理进程间通讯;
  • QAbstractSocket:所有套接字类的父类;
  • QTcpSocket:TCP协议网络数据传输;
  • QUdpSocket:传输 UDP 报文;
  • QSslSocket:使用 SSL/TLS 传输数据;

QFile提供了从文件中读取和写入数据的能力。

我们通常会将文件路径作为参数传给QFile的构造函数。不过也可以在创建好对象最后,使用setFileName()来修改

我们可以使用QDataStream或QTextStream类来读写文件,也可以使用QIODevice类提供的read()、readLine()、readAll()以及write()这样的函数。值得注意的是,有关文件本身的信息,比如文件名、文件所在目录的名字等,则是通过QFileInfo获取

QFileInfo有很多类型的函数,举一些常用例子。比如:

  • isDir()检查该文件是否是目录;
  • isExecutable() 检查该文件是否是可执行文件等。
  • baseName() 可以直接获得文件名;
  • completeBaseName() 获取完整的文件名
  • suffix() 则直接获取文件后缀名。
  • completeSuffix() 获取完整的文件后缀

文件

1、QFile file;

file.setFileName();

file.open();

file.write();

file.read();

file.close();

2、QFileInfo info;

info.size();

info.fileName();



QDataStream:二进制方式

QTextStream:文本方式(指定编码)

QBuffer:内存文件(内容放在内存)

八、Socket通信

Qt中提供的所有的Socket类都是非阻塞的。

Qt中常用的用于socket通信的套接字类:

  • QTcpServer 用于TCP通信, 作为服务器端套接字使用

  • QTcpSocket 用于TCP通信,作为客户端套接字使用。

  • QUdpSocket 用于UDP通信,服务器,客户端均使用此套接字。

TCP

在Qt中实现TCP服务器端通信的流程:

  1. 创建套接字

  2. 将套接字设置为监听模式

  3. 等待并接受客户端请求

    可以通过QTcpServer提供的void newConnection()****信号来检测是否有连接请求,如果有可以在对应的槽函数中调用nextPendingConnection函数获取到客户端的Socket信息(返回值为QTcpSocket*类型指针),通过此套接字与客户端之间进行通信。

  4. 接收或者向客户端发送数据

接收数据:使用read()或者readAll()函数

发送数据:使用write()函数

在Qt中实现TCP/IP客户端通信的流程:

  1. 创建套接字

  2. 连接服务器

    可以使用QTcpSocket类的connectToHost()函数来连接服务器。

  3. 向服务器发送或者接受数据

下面例子为简单的TCP/IP通信的实现例子:

TCP服务端

//---------- tcpserver.h ------------
class TCPServer : public QMainWindow
{
    Q_OBJECT

public:
    explicit TCPServer(QWidget *parent = 0);
    ~TCPServer();

public slots:
    void slotNewConnection();
    void slotReadyRead();

private:
    Ui::TCPServer *ui;
    // 负责监听的套接字
    QTcpServer* m_server;
    // 负责通信的套接字
    QTcpSocket* m_client;
};

//---------- tcpserver.cpp ------------
TCPServer::TCPServer(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::TCPServer),
    m_server(NULL),
    m_client(NULL)
{
    ui->setupUi(this);

    //创建套接字对象
    m_server = new QTcpServer(this);
    //将套接字设置为监听模式
    m_server->listen(QHostAddress::Any, 9999);

    //通过信号接收客户端请求
    connect(m_server, &QTcpServer::newConnection, 
this, &TCPServer::slotNewConnection);
}

TCPServer::~TCPServer()
{
    delete ui;
}

void TCPServer::slotNewConnection()
{
    if(m_client == NULL)
    {
        //处理客户端的连接请求
        m_client = m_server->nextPendingConnection();
        //发送数据
        m_client->write("服务器连接成功!!!");
        //连接信号, 接收客户端数据
        connect(m_client, &QTcpSocket::readyRead, 
this, &TCPServer::slotReadyRead);
    }
}

void TCPServer::slotReadyRead()
{
    //接收数据
    QByteArray array = m_client->readAll();
    QMessageBox::information(this, "Client Message", array);
}

TCP客户端

//------------- tcpclient.h ------------
class TCPClient : public QMainWindow
{
    Q_OBJECT

public:
    explicit TCPClient(QWidget *parent = 0);
    ~TCPClient();

public slots:
    void slotReadyRead();
    void slotSendMsg();

private:
    Ui::TCPClient *ui;
    QTcpSocket* m_client;
};

//------------- tcpclient.cpp --------------
TCPClient::TCPClient(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::TCPClient)
{
    ui->setupUi(this);
    //创建套接字
    m_client = new QTcpSocket(this);
    //连接服务器
    m_client->connectToHost(QHostAddress("127.0.0.1"), 9999);

    //通过信号接收服务器数据
    connect(m_client, &QTcpSocket::readyRead, 
this, &TCPClient::slotReadyRead);
    //发送按钮
    connect(ui->btnSend, &QPushButton::clicked, 
this, &TCPClient::slotSendMsg);
}

TCPClient::~TCPClient()
{
    delete ui;
}

void TCPClient::slotReadyRead()
{
     //接收数据
    QByteArray array = m_client->readAll();
    QMessageBox::information(this, "Server Message", array);
}

void TCPClient::slotSendMsg()
{
    QString text = ui->textEdit->toPlainText();
     //发送数据
    m_client->write(text.toUtf8());
    ui->textEdit->clear();
}

UDP

在UDP方式下,客户端并不与服务器建立连接,它只负责调用发送函数向服务器发送数据。类似的服务器也不从客户端接收连接,只负责调用接收函数,等待来自客户端的数据的到达

在UDP通信中,服务器端和客户端的概念已经显得有些淡化,两部分做的工作都大致相同:

  1. 创建套接字

  2. 绑定套接字

    在UDP中如果需要接收数据需要对套接字进行绑定,只发送数据则不需要对套接字进行绑定。

    通过调用bind()函数将套接字绑定到指定端口上。

  3. 接收或者发送数据

  • 接收数据:使用readDatagram()接收数据,函数声明如下:
qint64 readDatagram(char * data, qint64 maxSize, QHostAddress * address = 0, quint16 * port = 0)
参数:

data: 接收数据的缓存地址

maxSize: 缓存接收的最大字节数

address: 数据发送方的地址(一般使用提供的默认值)

port: 数据发送方的端口号(一般使用提供的默认值)

使用pendingDatagramSize()可以获取到将要接收的数据的大小,根据该函数返回值来准备对应大小的内存空间存放将要接收的数据。

  • 发送数据: 使用writeDatagram()函数发送数据,函数声明如下:
qint64 writeDatagram(const QByteArray & datagram, const QHostAddress & host, quint16 port)
参数:

datagram:要发送的字符串

host:数据接收方的地址

port:数据接收方的端口号

广播

在使用QUdpSocket类的writeDatagram()函数发送数据的时候,其中第二个参数host应该指定为广播地址:QHostAddress::Broadcast此设置相当于QHostAddress("255.255.255.255")

使用UDP广播的的特点:

  • 使用UDP进行广播,局域网内的其他的UDP用户全部可以收到广播的消息

  • UDP广播只能在局域网范围内使用

组播

在使用QUdpSocket类的writeDatagram()函数发送数据的时候,其中第二个参数host应该指定为组播地址,关于组播地址的分类:

  • 224.0.0.0~224.0.0.255为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用;

  • 224.0.1.0~224.0.1.255是公用组播地址,可以用于Internet;

  • 224.0.2.0~238.255.255.255为用户可用的组播地址(临时组地址),全网范围内有效;

  • 239.0.0.0~239.255.255.255为本地管理组播地址,仅在特定的本地范围内有效。

注册加入到组播地址需要使用QUdpSocket类的成员函数:

bool joinMulticastGroup(const QHostAddress & groupAddress)

TCP 和 UDP的区别

TCP UDP
是否连接 面向连接 无连接
传输方式 基于流 基于数据报
传输可靠性 可靠 不可靠
传输效率 效率低 效率高
能否广播 不能

九、多线程的使用

在次线程中处理的业务放在独立的模块(类)中,由主线程创建完该对象后,将其移交给指定的线程,且可以将多个类似的对象移交给同一个线程。

例子中,信号由主线程的QTimer对象发出,之后Qt会将关联的事件放到worker所属线程的事件队列。由于队列连接的作用,在不同线程间连接信号和槽是很安全的。

示例代码如下:

class Worker : public QObject
{
    Q_OBJECT
private slots:
    void onTimeout()
    {
        qDebug()<<"Worker::onTimeout get called from?: "
<<QThread::currentThreadId();
    }
};
    
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    qDebug()<<"From main thread: "<<QThread::currentThreadId();
  
    QThread t;
    QTimer timer;
    Worker worker;
  
    QObject::connect(&timer, SIGNAL(timeout()), 
&worker, SLOT(onTimeout()));
         // 启动定时器
    timer.start(1000);
     // 将类对象移交个线程
    worker.moveToThread(&t);
    // 启动线程
    t.start();
  
    return a.exec();
}

关于Qobject类的connect函数最后一个参数,连接类型:

  • 自动连接(AutoConnection),默认的连接方式。

如果信号与槽,也就是发送者与接受者在同一线程,等同于直接连接;

如果发送者与接受者处在不同线程,等同于队列连接。

  • 直接连接(DirectConnection)

当信号发射时,槽函数立即直接调用。无论槽函数所属对象在哪个线程,槽函数总在发送者所在线程执行。

  • 队列连接(QueuedConnection)

当控制权回到接受者所在线程的事件循环时,槽函数被调用。槽函数在接受者所在线程执行。

总结:

* 队列连接:槽函数在接受者所在线程执行

* 直接连接:槽函数在发送者所在线程执行

*自动连接:二者不在同一线程时,等同于队列连接

多线程使用过程中注意事项:

  • 线程不能操作UI对象(从Qwidget直接或间接派生的窗口对象)

  • 需要移动到子线程中处理的模块类,创建的对象的时候不能指定父对象。

十、Qt数据库操作

一、操作数据库

Qt 提供了 QtSql 模块来提供平台独立的基于 SQL 的数据库操作。

Qt 使用QSqlDatabase表示一个数据库连接。

可以通过:

//打印Qt支持的数据库驱动
qDebug() << QSqlDatabase::drivers();

找到系统中所有可用的数据库驱动的名字列表。

封装一个连接数据库的函数:

bool connect(const QString &dbName)
{
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
//    db.setHostName("host");
//    db.setDatabaseName("dbname");
//    db.setUserName("username");
//    db.setPassword("password");
    db.setDatabaseName(dbName);
    if (!db.open()) {
        QMessageBox::critical(0, QObject::tr("Database Error"),
                              db.lastError().text());
        return false;
    }
    return true;
}

创建数据库表student后,插入数据,然后将其独取出来:

if (connect("demo.db")) 
{
    QSqlQuery query;
    query.prepare("INSERT INTO student (name, age) VALUES (?, ?)");
    QVariantList names;
    names << "Tom" << "Jack" << "Jane" << "Jerry";
    query.addBindValue(names);
    QVariantList ages;
    ages << 20 << 23 << 22 << 25;
    query.addBindValue(ages);
    if (!query.execBatch()) {
        QMessageBox::critical(0, QObject::tr("Database Error"),
                              query.lastError().text());
    }
 
    query.exec("SELECT name, age FROM student");
    while (query.next()) {
        QString name = query.value(0).toString();
        int age = query.value(1).toInt();
        qDebug() << name << ": " << age;
    }
} 
else 
{
    return 1;
}

插入多条数据,此时可以使用QSqlQuery::exec()函数一条一条插入数据,但是这里我们选择了另外一种方法:批量执行。首先,我们使用QSqlQuery::prepare()函数对这条 SQL 语句进行预处理,问号 ? 相当于占位符,预示着以后我们可以使用实际数据替换这些位置。

在上面的代码中,我们使用一个字符串列表 names 替换掉第一个问号的位置,一个整型列表 ages 替换掉第二个问号的位置,利用QSqlQuery::addBindValue()我们将实际数据绑定到这个预处理的 SQL 语句上。需要注意的是,names 和 ages 这两个列表里面的数据需要一一对应。然后我们调用QSqlQuery::execBatch()批量执行 SQL,之后结束该对象。这样,插入操作便完成了。

二、使用模型操作数据库

基于QSqlTableModel 的模型处理更为高级,如果对 SQL 语句不熟悉,并且不需要很多复杂的查询,这种QSqlTableModel模型基本可以满足一般的需求。

1、查询操作

if (connect("demo.db")) 
{
    QSqlTableModel model;
    model.setTable("student");
    model.setFilter("age > 20 and age < 25");
    if (model.select()) {
        for (int i = 0; i < model.rowCount(); ++i) {
            QSqlRecord record = model.record(i);
            QString name = record.value("name").toString();
            int age = record.value("age").toInt();
            qDebug() << name << ": " << age;
        }
    }
} 
else 
{
    return 1;
}
  • setTable()函数设置所需要操作的表格;

  • setFilter()函数则是添加过滤器,也就是 WHERE 语句所需要的部分。

    例如上面代码中的操作实际相当于 SQL 语句:

    SELECT * FROM student WHERE age > 20 and age < 25
    

注意:我们使用QSqlTableModel只能进行 SELECT * 的查询,不能只查询其中某些列的数据。

2、插入操作

QSqlTableModel model;
model.setTable("student");
int row = 0;
model.insertRows(row, 1);
model.setData(model.index(row, 1), "Cheng");
model.setData(model.index(row, 2), 24);
model.submitAll();

model.insertRows(row, 1);说明我们想在索引 0 的位置插入 1 行新的数据。使用setData()函数则开始准备实际需要插入的数据。最后,调用submitAll()函数提交所有修改

例如上面代码中的操作实际相当于 SQL 语句:

INSERT INTO student (name, age) VALUES ('Cheng', 24)

3、更新操作

QSqlTableModel model;
model.setTable("student");
model.setFilter("age = 25");
if (model.select()) {
    if (model.rowCount() == 1) {
        QSqlRecord record = model.record(0);
        record.setValue("age", 26);
        model.setRecord(0, record);
        model.submitAll();
    }
}

找到 age = 25 的记录,然后将 age 重新设置为 26,存入相同的位置(在这里都是索引 0 的位置),提交之后完成一次更新

例如上面代码中的操作实际相当于 SQL 语句:

UPDATE student SET age = 26 WHERE age = 25

4、删除操作

QSqlTableModel model;
model.setTable("student");
model.setFilter("age = 25");
if (model.select()) {
    if (model.rowCount() == 1) {
        model.removeRows(0, 1);
        model.submitAll();
    }
}

removeRows()函数可以一次删除多行。

例如上面代码中的操作实际相当于 SQL 语句:

DELETE FROM student WHERE age = 25

如果你觉得文章还不错,可以给个"三连",文章同步到个人微信公众号[加班猿]

我是hackett,我们下期见

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容