Qt 事件系統總結

来源:https://www.cnblogs.com/Critical-Thinking/archive/2023/06/12/17474464.html
-Advertisement-
Play Games

> 參考: > > - [(35條消息) Qt事件迴圈及QEventLoop的使用_kupeThinkPoem的博客-CSDN博客](https://blog.csdn.net/kupepoem/article/details/121844578) > - [(35條消息) Qt消息機制:事件分發和 ...


參考:

Qt 事件系統總結

Qt 事件

  • 在 Qt 中,事件(event)是一些對象,它們都派生自抽象類 QEvent

  • 事件是應用程式所關心的,程式內部發生的事或是外部行動的結果

  • 當一個事件發生,Qt 會創建一個事件對象,它是一個派生自抽象類 QEvent 的類的實例,用來代表發生的事件

  • 有時一個事件包含多個事件類型,比如滑鼠事件 QMouseEvent 又可以分為滑鼠按下雙擊滾輪滾動移動等多種操作

  • 事件由誰接收:事件可以被任何派生自 QObject 的類型的實例接收和處理

    • QObject 類的三大核心功能其中之一就是:事件處理。QObject通過 event() 函數獲取和分發事件。
  • 事件由誰產生:

    • 由操作系統或應用程式內部產生
    • 使用 bool QEvent::spontaneous() const 判斷事件是否來自於應用程式外部,如果事件來自於外部返回 true,否則返回 false

Qt 事件迴圈

主事件迴圈

  • 每一個 Qt 程式,main 函數中一般都有唯一的 QCoreApplication/QGuiApplication/QApplication,併在末尾調用 exec()。這樣就開始 Qt 的事件迴圈
  • 事件迴圈的本質是無限迴圈,使用 exec() 開啟事件迴圈,如果事件迴圈不結束,exec() 後面的代碼永遠不會執行。
  • 在執行 exec() 函數之後,程式將進入事件迴圈來監聽應用程式的事件。事件多數情況下是被分發到一個隊列中(事件隊列),當隊列中有事件時就不停的將隊列中的事件發送給 QObject 對象,當隊列為空時就迴圈等待事件。
  • 當事件發生時,Qt 將創建一個事件對象。Qt 中所有事件類都繼承於 QEvent ,這也是事件不同於信號(信號與槽中的信號)的一點 —— 事件是類具有特定類型, 而信號是信號函數
  • QCoreApplication 中提供了一下處理事件的函數:
/// 給任何線程的任何對象發送任何事件都會調用該函數。可以重寫該函數來達到全局的事件處理與控制的功能。
[virtual] bool QCoreApplication::notify(QObject *receiver, QEvent *event)

/// 直接使用 notify() 將事件發送給事件的接收者,返回事件處理程式返回的值。事件被髮送後並不會被自動被銷毀,因此事件對象常常可以聲明在堆棧上作為自動變數。
[static] bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event)
    
/// 添加事件到事件隊列然後立即返回。事件必須聲明在堆上。當控制返回到主事件迴圈時,所有存儲在事件隊列中的事件都將使用 notify 函數發送出去。
/// 事件按優先順序排隊,高優先順序的事件先入隊。事件優先順序是一個整機變數。
/// 函數是【線程安全】的
[static] void QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority = Qt::NormalEventPriority)
    
/// 立即分派在事件隊列中的所有事件接收對象為 receiver 事件類型為 event_type 的事件。
/// 如果 receiver = nullptr ,所有事件類型為 event_type 都會被立即發送給接收者
/// 如果 event_type = 0, 所有發送給 receiver 的事件都會被立即發送給它
[static] void QCoreApplication::sendPostedEvents(QObject *receiver = nullptr, int event_type = 0)
    
/// 告訴應用以指定的返回碼退出事件迴圈,exec() 將結束並返回該返回碼,任何非零返回碼意味著錯誤。
[static] void QCoreApplication::exit(int returnCode = 0)

/// 告訴應用正常退出事件迴圈。相當於 exit(0)。通常信號與該槽應該進行[隊列連接],因為如果在主事件迴圈開始之前,信號發送導致的 quit() 回調是無效的(事件迴圈沒有開始,何談退出)
/// 使用隊列連接確保槽函數不會再事件迴圈開始前執行。
[static slot] void QCoreApplication::quit()
    

QEventLoop 類

/// 開啟事件迴圈 
int exec(QEventLoop::ProcessEventsFlags flags = AllEvents)
void exit(int returnCode = 0)
/// 如果事件迴圈是運行著的,返回 true,否則返回 false。事件迴圈在 exec() 和 exit() 之間被認為是運行的
bool isRunning() const
[slot] void QEventLoop::quit()
  • 事件迴圈是可以嵌套的,當在子事件迴圈中的時候,父事件迴圈中的事件實際上處於中斷狀態。這就相當於迴圈嵌套。

  • 當子事件迴圈結束,exec() 返回之後才可以執行父迴圈中的事件。當然,這不代表在執行子迴圈的時候,類似父迴圈中的界面響應會被中斷,因為往往子迴圈中也會有父迴圈的大部分事件,執行QMessageBox::exec(),QEventLoop::exec()的時候,雖然這些exec()打斷了main()中的QApplication::exec(),但是由於GUI界面的響應已經被包含到子迴圈中了,所以GUI界面依然能夠得到響應。

  • 如果某個子事件迴圈仍然有效,但其父迴圈被強制跳出,此時父迴圈不會立即執行跳出,而是等待子事件迴圈跳出後,父迴圈才會跳出。

事件的轉發與處理流程

Qt 程式需要在 main() 函數創建一個 QApplication 對象,然後調用它的 exec() 函數。這個函數就是開始 Qt 的事件迴圈。在執行 exec() 函數之後,程式將進入事件迴圈來監聽應用程式的事件

image-20230608165523292
事件迴圈與事件隊列
image-20230608203644315
事件的轉發與處理流程

同步與非同步事件

  • 同步事件: 調用 QCoreApplication::sendEvent() ,會直接使用 QCoreApplication::notify 將事件發送給事件接收方,事件會立即被執行。
  • 非同步事件:調用 QCoreApplication::postEvent(), 會將事件加入到事件隊列,等待事件迴圈進行處理。

事件分發器

Qt 中每個事件類型都有一個枚舉類型 QEvent::Type 的數據成員,通過該枚舉類型,在程式中可以區分不同的事件類型,根據不同的事件類型進行不同的動作。如下即為 QObject::event 的源碼:

  • 事件分發器根據事件的不同,將事件發送給不同的事件處理器進行處理,因此可以通過重寫事件處理器函數,讓指定事件發生髮生時,執行我們想要的事件處理動作。
bool QObject::event(QEvent *e)
{
    switch (e->type()) {
    case QEvent::Timer:
        timerEvent((QTimerEvent *)e);
        break;

    case QEvent::ChildAdded:
    case QEvent::ChildPolished:
    case QEvent::ChildRemoved:
        childEvent((QChildEvent *)e);
        break;

    case QEvent::DeferredDelete:
        qDeleteInEventHandler(this);
        break;

    case QEvent::MetaCall:
        {
            QAbstractMetaCallEvent *mce = static_cast<QAbstractMetaCallEvent*>(e);

            if (!d_func()->connections.loadRelaxed()) {
                QBasicMutexLocker locker(signalSlotLock(this));
                d_func()->ensureConnectionData();
            }
            QObjectPrivate::Sender sender(this, const_cast<QObject*>(mce->sender()), mce->signalId());

            mce->placeMetaCall(this);
            break;
        }

    case QEvent::ThreadChange: {
        Q_D(QObject);
        QThreadData *threadData = d->threadData.loadRelaxed();
        QAbstractEventDispatcher *eventDispatcher = threadData->eventDispatcher.loadRelaxed();
        if (eventDispatcher) {
            QList<QAbstractEventDispatcher::TimerInfo> timers = eventDispatcher->registeredTimers(this);
            if (!timers.isEmpty()) {
                // do not to release our timer ids back to the pool (since the timer ids are moving to a new thread).
                eventDispatcher->unregisterTimers(this);
                QMetaObject::invokeMethod(this, "_q_reregisterTimers", Qt::QueuedConnection,
                                          Q_ARG(void*, (new QList<QAbstractEventDispatcher::TimerInfo>(timers))));
            }
        }
        break;
    }

    default:
        if (e->type() >= QEvent::User) {
            customEvent(e);
            break;
        }
        return false;
    }
    return true;
}
  • 如果希望在事件分發之前做一些操作,就可以重寫這個 event() 函數。
  • 如果傳入的事件已被識別並且處理,則需要返回 true,否則返回 false。如果返回值是 true,那麼 Qt 會認為這個事件已經處理完畢,不會再將這個事件發送給其它對象,而是會繼續處理事件隊列中的下一事件

❗❗❗在 event() 函數中,調用事件對象的 accept()ignore() 函數是沒有作用的,不會影響到事件的傳播

事件過濾器

事件過濾器可以對其他組件接收到的事件進行監控

事件過濾器使用步驟如下:

  1. 創建一個事件過濾器

    • 任意的 QObject 對象都可以作為事件過濾器使用

    • 事件過濾器對象需要重寫 eventFilter() 函數

      • eventFilter() 中可以決定是否將事件傳遞給組件對象,事件處理程式也可以提前寫在事件過濾器中。
      • 不讓事件繼續轉發返回 true, 否則返回 false
  2. 被監控對象安裝事件過濾器

    • void QObject::installEventFilter(QObject *filterObj)

img

事件過濾器的調用時間是目標對象(也就是 eventFilter() 參數裡面的 watched 對象)接收到事件對象之前。如果事件被過濾掉(返回 true) 那麼組件對象就不會收到該事件。

❗❗❗ 事件過濾器和被安裝過濾器的組件必須在同一線程,否則,過濾器將不起作用。另外,如果在安裝過濾器之後,這兩個組件到了不同的線程,那麼,只有等到二者重新回到同一線程的時候過濾器才會有效

全局事件過濾器QAppliaction::instance()QCoreApplication::instance() 上安裝事件過濾器,那麼任何事件在通過 notify() 函數發送給其他對象之前都要先傳給事件過濾器。

Qt 事件處理的 5 個層次

  1. 重寫 paintEvent()mousePressEvent() 等事件處理函數。最普通、最簡單的形式。
  2. 重寫 event() 函數。event() 是任何 Qt 對象的所有事件的入口,預設是根據事件類型的不同將事件分發給不同的事件處理函數
  3. 在特定對象上安裝事件過濾器,該事件過濾器僅過濾該對象接收到的事件
  4. 使用全局事件過濾器,在 QAppliaction::instance()QCoreApplication::instance() 上安裝事件過濾器。事件過濾器可以安裝多個(多個事件過濾器會按安裝順序逆序激活),相比重寫 notify() 更加靈活,全局過濾器有一個問題:只能用在主線程。
  5. 重寫 QCoreApplication::notify() 這是最強大的,和全局事件過濾器一樣提供完全控制,並且不受線程的限制。但是全局範圍內只能有一個被使用(因為QCoreApplication是單例的)。

事件(QEvent)與信號(SIGNAL)的區別

事件 信號
本質區別 事件是對象,都是派生自 QEvent 的類的實例 信號是QObject 或是其派生類的函數成員
與 QObject 的關係 事件由 QObject 及其派生類的實例對象接收併進行處理 信號由QObject 或是其派生類的實例對象發出(emit
對程式影響 改寫事件處理函數可能導致程式行為發生改變 如果將信號與不同的槽函數連接,會導致不同的行為
  • 兩者的聯繫:
    • 事件的轉發和處理,信號與槽實現的對象間通訊,都依靠於 QObject,都依靠 Qt 的事件迴圈。
    • 一些信號,是在事件處理函數中發出的。

QPushButton 事件處理分析

示例代碼:

#ifndef MYAPPLICATION_H
#define MYAPPLICATION_H
#include <QApplication>

class MyApplication : QApplication
{
    Q_OBJECT
public:
    MyApplication(int &argc, char **argv);
    bool notify(QObject *receiver, QEvent *event) override;
    void installEventFilter(QObject *filter);
    int exec();
protected:
    bool event(QEvent *e) override;
};

#endif // MYAPPLICATION_H
#include "myapplication.h"
#include "qdebug.h"


MyApplication::MyApplication(int &argc, char **argv)
    : QApplication{argc, argv}
{

}

bool MyApplication::notify(QObject *receiver, QEvent *event)
{
    if(event->type() == QEvent::MouseButtonPress){
        qDebug() << "MyApplication::notify():發佈滑鼠按下事件給類型為 " << receiver->metaObject()->className()
                 << " 的對象;";
    }
    return QApplication::notify(receiver, event);
}

void MyApplication::installEventFilter(QObject *filter)
{
    QApplication* a = static_cast<QApplication*>(this);
    a->installEventFilter(filter);
}

int MyApplication::exec()
{
    return QApplication::exec();
}

bool MyApplication::event(QEvent *e)
{
    if(e->type() == QEvent::MouseButtonPress){
        qDebug() << "MyApplication::event(): 分發滑鼠按下事件";
    }
    return QApplication::event(e);
}
#ifndef MYBUTTON_H
#define MYBUTTON_H

#include <QPushButton>

class MyButton : public QPushButton
{
    Q_OBJECT
public:
    MyButton(QWidget* parent = nullptr);
    MyButton(QString const& text, QWidget* parent = nullptr);
protected:
    void mousePressEvent(QMouseEvent *e) override;
    bool event(QEvent* e) override;
};

#endif // MYBUTTON_H
#include "mybutton.h"
#include <QDebug>
#include <QEvent>
#include <QMouseEvent>

MyButton::MyButton(QWidget* parent)
    : QPushButton{parent}
{

}

MyButton::MyButton(const QString &text, QWidget *parent)
    : QPushButton{text, parent}
{

}

void MyButton::mousePressEvent(QMouseEvent *e)
{
    qDebug() << "MyButton::mousePressEvent() 按鈕按下事件被處理";

    QPushButton::mousePressEvent(e);// 預設的事件處理函數中,會發出信號: emit pressed()

    qDebug() << "槽函數回調返回後,執行發送信號(emit)後面的代碼";
}

bool MyButton::event(QEvent *e)
{
    if(e->type() == QEvent::MouseButtonPress){
        qDebug() << "MyButton::event(): 按鈕點擊事件被分發";
    }
    return QPushButton::event(e);
}

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include "mybutton.h"

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

protected:
    bool event(QEvent *e) override;
    bool eventFilter(QObject* watched, QEvent *event) override;
private:
    MyButton *button;
private slots:
    void buttonPressedSlot();
};
#endif // WIDGET_H
#include "widget.h"
#include <QEvent>
#include <QMouseEvent>
#include <QDebug>
#include <QPushButton>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    button = new MyButton(">>> 按鈕 <<<",this);
    button->installEventFilter(this);
    connect(button, &MyButton::pressed, this, &Widget::buttonPressedSlot);
}

Widget::~Widget()
{
}

bool Widget::event(QEvent *e)
{
    if(e->type() == QEvent::MouseButtonPress){
        qDebug() << "Widget::event(): 按鈕按下事件被分發";
    }
    return QWidget::event(e);
}

bool Widget::eventFilter(QObject* watched, QEvent *event)
{
    if(watched == this->button && event->type() == QEvent::MouseButtonPress){
        qDebug() << "在滑鼠按下事件發送給 button 之前,事件過濾器 Widget::eventFilter() 先對事件進行處理";
    }
    return QWidget::eventFilter(watched, event);
}

void Widget::buttonPressedSlot(){
    qDebug() << "Widget::buttonPressedSlot(): 按下按鈕的槽函數被調用";
}
#include "widget.h"
#include "myapplication.h"
#include <QDebug>
#include <memory.h>

class GlobalFilter : public QObject
{
public:
    GlobalFilter(QObject *parent = nullptr) : QObject{parent}{}
protected:
    bool eventFilter(QObject* watched, QEvent *event) override{
        if(event->type() == QEvent::MouseButtonPress){
            qDebug() << "在滑鼠按下事件發送給類型為 " << watched->metaObject()->className()
                     << " 的對象之前,全局事件過濾器 GlobalFilter::eventFilter() 先對事件進行處理;";
        }
        return QObject::eventFilter(watched, event);
    }
};

int main(int argc, char *argv[])
{
    MyApplication a(argc, argv);
    std::unique_ptr<GlobalFilter> uptr_filter(new GlobalFilter);
    a.installEventFilter(uptr_filter.get());
    Widget w;
    w.show();
    return a.exec();
}

image-20230609162238213

輸出:

# 點擊按鈕輸出如下信息:
MyApplication::notify():發佈滑鼠按下事件給類型為  QWidgetWindow  的對象;
在滑鼠按下事件發送給類型為  QWidgetWindow  的對象之前,全局事件過濾器 GlobalFilter::eventFilter() 先對事件進行處理;
MyApplication::notify():發佈滑鼠按下事件給類型為  MyButton  的對象;
在滑鼠按下事件發送給類型為  MyButton  的對象之前,全局事件過濾器 GlobalFilter::eventFilter() 先對事件進行處理;
在滑鼠按下事件發送給 button 之前,事件過濾器 Widget::eventFilter() 先對事件進行處理
MyButton::event(): 按鈕點擊事件被分發
MyButton::mousePressEvent() 按鈕按下事件被處理
Widget::buttonPressedSlot(): 按下按鈕的槽函數被調用
槽函數回調返回後,執行發送信號(emit)後面的代碼

示例:查看圖片的簡單應用

功能如下:

  • 選擇和打開圖片:點擊按鈕,選擇打開一個圖片
  • 縮放圖片:通過滑鼠滾輪能夠縮放圖片,縮放中心為圖片繪製視窗的幾何中心
    • 相關事件:wheelEvent()QEvent::ResizeQEvent::Paint
  • 拖動圖片:通過滑鼠左鍵按住來拖動圖片
    • 相關事件:mousePressEvent()mouseMoveEvent()QEvent::Paint
image-20230612105958478
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();
protected:
    /// 重寫事件過濾器,在事件過濾器中來處理子視窗的繪圖事件
    /// 預設的事件過濾器默會把把父視窗下子控制項的繪圖事件過濾掉,因此重新父視窗的 paintEvent 是無法在子控制項上繪圖的。
    /// 因此,直接在事件過濾器中處理繪圖事件
    bool eventFilter(QObject *watched, QEvent *event) override;
    void mousePressEvent(QMouseEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;
    void wheelEvent(QWheelEvent *event) override;

private:
    /// 設置圖片的縮放中心
    /// 輸入縮放中心在圖窗中的坐標,來求更新 zoomCenter 和 zoomCenterPic
    void setZoomCenter(const QPointF& zoomCenter);
    /// 因為圖片縮放預設以圖片坐標系原點(圖片左上角頂點)為縮放中心
    /// 為了以 zoomCenter 為縮放中心,需要修改圖片的繪製位置,
    /// 通過縮放 + 移動使得圖片相當於以指定的縮放中心縮放
    void correctImagPosition();
    Ui::Widget *ui;
    QPixmap pixmap;
    double scaleFactor;     ///< 縮放因數
    QPointF zoomCenter;     ///< 圖片縮放中心在圖窗坐標系下的坐標
    QPointF zoomCenterPic;  ///< 當縮放因數為 1 時,縮放中心相對於圖片坐標系的坐標
    QPointF posit;          ///< 圖片繪製位置(為圖片左上角頂點在圖窗中的坐標)
    bool isImgError;        ///< 讀取圖片是否錯誤的標誌
    QPoint lastDragPos;     ///< 暫存滑鼠最新的拖動位置


private slots:
    void openImg();
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
#include <QFileDialog>
#include <QPainter>
#include <QMouseEvent>
#include <cmath>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
    , scaleFactor(1)
    , isImgError(false)
{
    ui->setupUi(this);
    ui->widget->installEventFilter(this);// 為圖片顯示視窗安裝事件過濾器
    connect(ui->pushButton, &QPushButton::clicked, this, &Widget::openImg);
}

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

bool Widget::eventFilter(QObject *watched, QEvent *event)
{
    if(watched == ui->widget && event->type() == QEvent::Paint){
        // 處理子視窗 ui->widget 的 Paint 事件
        QPainter painter(ui->widget);
        if(pixmap.isNull()){
            painter.fillRect(rect(), Qt::black);
            painter.setPen(Qt::white);
            if(isImgError){
                painter.drawText(rect(), Qt::AlignCenter, tr("無法打開圖片"));
                return true;
            }else{
                painter.drawText(rect(), Qt::AlignCenter, tr("請選擇圖片"));
                return true;
            }
        }
        // 縮放圖片
        QPixmap img = pixmap.scaled(pixmap.width() / scaleFactor, pixmap.height() / scaleFactor, Qt::KeepAspectRatio/*, Qt::SmoothTransformation*/);
        correctImagPosition();// 修正圖片位置
        painter.drawPixmap(posit, img);// 繪製圖片
    }else if(watched == ui->widget && event->type() == QEvent::Resize){
        // 處理子視窗 ui->widget 的 Resize 事件
        QResizeEvent* re = static_cast<QResizeEvent*>(event);
        setZoomCenter(QPointF(re->size().width() / 2.0, re->size().height() / 2.0));// 更新縮放中心位置
        return false;
    }else{

        return QWidget::eventFilter(watched,event);//其它事件交給父類事件過濾器處理
    }
}

void Widget::mousePressEvent(QMouseEvent *event)
{
    if(event->button() == Qt::LeftButton)
        lastDragPos = event->pos();// 暫存滑鼠按下時的位置
}

void Widget::mouseMoveEvent(QMouseEvent *event)
{
    if (event->buttons() & Qt::LeftButton) {
        QPoint delta = event->pos() - lastDragPos;// 計算滑鼠拖拽時的相對位置變化
        posit.rx() += delta.x();
        posit.ry() += delta.y();
        // 因為 posit 改變,縮放中心相對於圖片坐標系的位置也發生了改變
        zoomCenterPic -= delta * scaleFactor;//或:setZoomCenter(QPointF(this->width() / 2.0, this->height() / 2.0));
        lastDragPos = event->pos();
        update();
    }
}

void Widget::wheelEvent(QWheelEvent *event)
{
    // 滾輪朝前推為正,朝後推為負
    // 滑鼠滾輪每滾動 1°,angleDelta() 值加或減 8
    const int numDegrees = event->angleDelta().y() / 8;
    // 實際中,滾輪每滾動一格,角度變化為 15°
    const double numSteps = numDegrees / double(15);
    // 根據滾輪的移動格數修改縮放因數
    double tmp = scaleFactor * pow(0.8, numSteps);
    
    // 限制縮放因數的範圍
    if(tmp < 0.1)
        scaleFactor = pow(0.8, 10);
    else if(tmp > 10)
        scaleFactor = pow(0.8, -10);
    else
        scaleFactor = tmp;
    
    update();
}

void Widget::setZoomCenter(const QPointF& zoomCenter)
{
    this->zoomCenter = zoomCenter;
    this->zoomCenterPic = (zoomCenter - posit) * scaleFactor;
}

void Widget::correctImagPosition()
{
    // 縮放時修正圖片繪製位置
    posit = zoomCenter - zoomCenterPic / scaleFactor;
}

void Widget::openImg()
{
    QString fileName = QFileDialog::getOpenFileName(this, tr("選擇圖片"),
                                                    QDir::homePath(),
                                                    tr("Images (*.png *.xpm *.jpg)"));
    if(!pixmap.load(fileName)){
        isImgError = true;
    }else{
        isImgError = false;
        // 每次打開圖片,設置初始縮放因數為 1
        scaleFactor = 1;
        // 設置圖片初始在圖窗中心顯示
        posit.rx() = (this->width() - pixmap.width()) / 2.0;
        posit.ry() = (this->height() - pixmap.height()) / 2.0;
        // 設置圖窗中心為縮放中心
        setZoomCenter(QPointF(this->width() / 2.0, this->height() / 2.0));
    }
    update();// 更新視窗顯示
}
image-20230612105833568

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 大家好呀,我是小樓。 本文是上篇文章[《使用增強版 singleflight 合併事件推送,效果炸裂!》](https://mp.weixin.qq.com/s/PFojA2DWJF7ry9Rdu8znyA)的續集,沒看過前文必須要先看完才能看本文,實在不想看,拉到文章末尾,給我點個贊再退出吧~Do ...
  • 一個字典可能只包含幾個鍵值對,也可能包含數百萬個鍵值對,所以Python支持字典遍歷。字典可用於以各種方式存儲信息,因此有多種遍歷字典的方式:可遍歷字典的所有鍵值對、鍵或值。 # 1.遍歷所有的鍵值對 其語法格式: ![image](https://img2023.cnblogs.com/blog/ ...
  • 來源:cnblogs.com/zhangyinhua/p/11545305.html ## 一、BigDecimal概述 Java在java.math包中提供的API類BigDecimal,用來對超過16位有效位的數進行精確的運算。雙精度浮點型變數double可以處理16位有效數,但在實際應用中,可 ...
  • # %00截斷 **介紹:** > 0x00,%00,/00 在url中 %00 表示ascll碼中的 0 ,而ascii中0作為特殊字元保留,表示字元串結束,所以當url中出現%00時就會認為讀取已結束。但是所謂的if攔截仍會讀取後面的尾碼達到繞過白名單的效果。 當前版本環境: PHP版本低於5. ...
  • > 本文介紹了一個簡單的學生信息管理系統,包括管理員登錄、重置學生密碼、添加、刪除和修改學生信息、查詢學生信息以及對學生成績進行排序等功能。該系統使用Python編寫,基於控制台交互 ## 實現思路 > 該系統分為兩個部分,管理員登錄和學生信息管理。在管理員登錄時,程式會要求用戶輸入用戶名和密碼進行 ...
  • 題目:生產者-消費者問題演算法的設計與實現 目 錄 1. 課題概述... 2 2. 合作分工... 2 3. 相關知識... 2 4. 系統分析... 2 5. 系統設計... 2 6. 系統運行測試界面截圖... 2 7. 總結與心得體會... 2 8. 源程式清單... 2 1. 課題概述 1.1 ...
  • ## 教程簡介 XML是一種簡單的基於文本的語言,旨在以純文本格式存儲和傳輸數據。它代表可擴展標記語言。 [Java XML入門教程](https://www.itbaoku.cn/tutorial/java_xml-index.html) - 從基本到高級概念的簡單步驟瞭解Java XML,其中包 ...
  • ## 教程簡介 Babel是一個JavaScript編譯器,允許開發人員使用最前沿的JavaScript編寫代碼,然後Babel將其轉換為老式的JavaScript,讓更多的瀏覽器能夠理解。 [BabelJS入門教程](https://www.itbaoku.cn/tutorial/babeljs- ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...