Qt源碼閱讀(四) 事件迴圈

来源:https://www.cnblogs.com/codegb/archive/2023/03/30/17274163.html
-Advertisement-
Play Games

事件系統 文章為本人理解,如有理解不到位之處,煩請各位指正。 @ Qt的事件迴圈,應該是所有Qter都避不開的一個點,所以,這篇博客,咱們來瞭解源碼中一些關於Qt中事件迴圈的部分。 先拋出幾個疑問,根據源代碼,下麵一一進行解析。 事件迴圈是什麼? 事件是怎麼產生的? 事件是如何處理的? 什麼是事件循 ...


事件系統

文章為本人理解,如有理解不到位之處,煩請各位指正。

@

目錄
Qt的事件迴圈,應該是所有Qter都避不開的一個點,所以,這篇博客,咱們來瞭解源碼中一些關於Qt中事件迴圈的部分。
先拋出幾個疑問,根據源代碼,下麵一一進行解析。

  1. 事件迴圈是什麼?
  2. 事件是怎麼產生的?
  3. 事件是如何處理的?

什麼是事件迴圈?

對於Qt事件迴圈個人理解是,事件迴圈是一個隊列去迴圈處理事件。當隊列中有事件時,則去處理事件,如果沒有事件時,則會阻塞等待。

事件是如何產生的?

事件的產生可以分為兩種:

  1. 程式外部產生
  2. 程式內部產生

程式外部所產生的事件主要是指系統產生的事件,比如說滑鼠按下(MouseButtonPress)、按鍵按下(KeyPress)等,Qt捕捉系統的事件,然後將系統事件封裝成自己的QEvent類,再將事件發送出去。

程式內部產生的事件主要指我們在代碼里,手動創建一個事件,然後將事件通過sendEvent/postEvent,來發送到事件迴圈中。而sendEventpostEvent區別又在於一個是阻塞的(sendEvent)一個是非阻塞的(postEvent)。

我們結合源碼分析,看一下sendEventpostEvent分別幹了什麼導致一個是阻塞的一個是非阻塞的。

sendEvent

完整源碼如下:

bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event)
{
	// sendEvent是阻塞調用
    Q_TRACE(QCoreApplication_sendEvent, receiver, event, event->type());

    if (event)
        event->spont = false;
    return notifyInternal2(receiver, event);
}

可以看到,sendEvent是調用了notifyInternal2這個函數

bool QCoreApplication::notifyInternal2(QObject *receiver, QEvent *event)
{
	...
    // Qt enforces the rule that events can only be sent to objects in
    // the current thread, so receiver->d_func()->threadData is
    // equivalent to QThreadData::current(), just without the function
    // call overhead.
    // 事件只能在同一個線程被send
    QObjectPrivate *d = receiver->d_func();
    QThreadData *threadData = d->threadData;
    QScopedScopeLevelCounter scopeLevelCounter(threadData);
    if (!selfRequired)
        return doNotify(receiver, event);
    return self->notify(receiver, event);
}

進一步跟蹤到其doNotify函數

static bool doNotify(QObject *receiver, QEvent *event)
{
    if (receiver == nullptr) {                        // serious error
        qWarning("QCoreApplication::notify: Unexpected null receiver");
        return true;
    }

#ifndef QT_NO_DEBUG
	// 檢查接受線程與當前是否同線程
    QCoreApplicationPrivate::checkReceiverThread(receiver);
#endif

	// QWidget類必須用QApplication
    return receiver->isWidgetType() ? false : QCoreApplicationPrivate::notify_helper(receiver, event);
}

再到QCoreApplicationPrivate::notify_helper

bool QCoreApplicationPrivate::notify_helper(QObject *receiver, QEvent * event)
{
    // Note: when adjusting the tracepoints in here
    // consider adjusting QApplicationPrivate::notify_helper too.
    Q_TRACE(QCoreApplication_notify_entry, receiver, event, event->type());
    bool consumed = false;
    bool filtered = false;
    Q_TRACE_EXIT(QCoreApplication_notify_exit, consumed, filtered);

    // send to all application event filters (only does anything in the main thread)
    if (QCoreApplication::self
        && receiver->d_func()->threadData.loadRelaxed()->thread.loadAcquire() == mainThread()
        && QCoreApplication::self->d_func()->sendThroughApplicationEventFilters(receiver, event)) {
        filtered = true;
        return filtered;
    }
    // send to all receiver event filters
    if (sendThroughObjectEventFilters(receiver, event)) {
        filtered = true;
        return filtered;
    }

    // deliver the event
    // 直接調用對象的event函數,所以是阻塞的
    consumed = receiver->event(event);
    return consumed;
}

然後我們可以看到主要有幾個流程:

  1. 判斷QCoreApplication有沒有安裝事件過濾器,有就把信號發送到事件過濾器里,由事件過濾器對事件進行處理。

    // send to all application event filters (only does anything in the main thread)
    if (QCoreApplication::self
        && receiver->d_func()->threadData.loadRelaxed()->thread.loadAcquire() == mainThread()
        && QCoreApplication::self->d_func()->sendThroughApplicationEventFilters(receiver, event)) {
        filtered = true;
        return filtered;
    }
    
  2. 判斷事件接受對象,有沒有安裝事件過濾器,有就將信號發送到事件過濾器。

    // send to all receiver event filters
    if (sendThroughObjectEventFilters(receiver, event)) {
        filtered = true;
        return filtered;
    }
    

    具體遍歷事件接受對象所安裝的事件過濾器的代碼如下:

    bool QCoreApplicationPrivate::sendThroughObjectEventFilters(QObject *receiver, QEvent *event)
    {
        if (receiver != QCoreApplication::instance() && receiver->d_func()->extraData) {
            for (int i = 0; i < receiver->d_func()->extraData->eventFilters.size(); ++i) {
                QObject *obj = receiver->d_func()->extraData->eventFilters.at(i);
                if (!obj)
                    continue;
                if (obj->d_func()->threadData != receiver->d_func()->threadData) {
                    qWarning("QCoreApplication: Object event filter cannot be in a different thread.");
                    continue;
                }
                if (obj->eventFilter(receiver, event))
                    return true;
            }
        }
        return false;
    }
    

    我們可以看到,只要事件被一個事件過濾器所成功處理,那麼後續的事件過濾器就不會被響應。同時,參看Qt幫助手冊中有提及到:

    If multiple event filters are installed on a single object, the filter that was installed last is activated first.

    後插入的事件過濾器會被優先響應。 具體安裝事件過濾器,我們在後面進行分析。

  3. 直接調用事件接受對象的event函數進行處理。因為是直接調用的對象的event,所以說,sendEvent函數會阻塞等待。

        // deliver the event
        // 直接調用對象的event函數,所以是阻塞的
        consumed = receiver->event(event);
        return consumed
    

postEvent

完整代碼如下:

void QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority)
{
    Q_TRACE_SCOPE(QCoreApplication_postEvent, receiver, event, event->type());

	// 事件的接收者不能為空
    if (receiver == nullptr) {
        qWarning("QCoreApplication::postEvent: Unexpected null receiver");
        delete event;
        return;
    }

	// 對事件接受對象所線上程的事件處理列表上鎖
    auto locker = QCoreApplicationPrivate::lockThreadPostEventList(receiver);
    if (!locker.threadData) {
        // posting during destruction? just delete the event to prevent a leak
        delete event;
        return;
    }

    QThreadData *data = locker.threadData;

    // if this is one of the compressible events, do compression
    // 將重覆的事件,進行壓縮
    if (receiver->d_func()->postedEvents
        && self && self->compressEvent(event, receiver, &data->postEventList)) {
        Q_TRACE(QCoreApplication_postEvent_event_compressed, receiver, event);
        return;
    }

    if (event->type() == QEvent::DeferredDelete)
        receiver->d_ptr->deleteLaterCalled = true;

    if (event->type() == QEvent::DeferredDelete && data == QThreadData::current()) {
        // remember the current running eventloop for DeferredDelete
        // events posted in the receiver's thread.

        // Events sent by non-Qt event handlers (such as glib) may not
        // have the scopeLevel set correctly. The scope level makes sure that
        // code like this:
        //     foo->deleteLater();
        //     qApp->processEvents(); // without passing QEvent::DeferredDelete
        // will not cause "foo" to be deleted before returning to the event loop.

        // If the scope level is 0 while loopLevel != 0, we are called from a
        // non-conformant code path, and our best guess is that the scope level
        // should be 1. (Loop level 0 is special: it means that no event loops
        // are running.)
        int loopLevel = data->loopLevel;
        int scopeLevel = data->scopeLevel;
        if (scopeLevel == 0 && loopLevel != 0)
            scopeLevel = 1;
        static_cast<QDeferredDeleteEvent *>(event)->level = loopLevel + scopeLevel;
    }

    // delete the event on exceptions to protect against memory leaks till the event is
    // properly owned in the postEventList
    QScopedPointer<QEvent> eventDeleter(event);
    Q_TRACE(QCoreApplication_postEvent_event_posted, receiver, event, event->type());
    data->postEventList.addEvent(QPostEvent(receiver, event, priority));
    eventDeleter.take();
    event->posted = true;
    ++receiver->d_func()->postedEvents;
    data->canWait = false;
    locker.unlock();

    QAbstractEventDispatcher* dispatcher = data->eventDispatcher.loadAcquire();
    if (dispatcher)
        dispatcher->wakeUp();
}
  1. 判斷事件接收對象是否為空

    // 事件的接收者不能為空
    if (receiver == nullptr) {
        qWarning("QCoreApplication::postEvent: Unexpected null receiver");
        delete event;
        return;
    }
    
  2. 將事件接收對象所線上程的post事件列表上鎖,如果已經被鎖了,就把事件刪除掉,並返回,防止泄露。

    // 對事件接受對象所線上程的事件處理列表上鎖
    auto locker = QCoreApplicationPrivate::lockThreadPostEventList(receiver);
    if (!locker.threadData) {
        // posting during destruction? just delete the event to prevent a leak
        delete event;
        return;
    }
    
  3. 將一些可以壓縮的事件進行壓縮,及多個事件壓縮成只推送最後的一個事件。Qt界面的update就是這個操作,為了防止多次刷新導致卡頓,短時間內多次的調用update可能只會刷新一次

    // if this is one of the compressible events, do compression
    // 將重覆的事件,進行壓縮
    if (receiver->d_func()->postedEvents
        && self && self->compressEvent(event, receiver, &data->postEventList)) {
        Q_TRACE(QCoreApplication_postEvent_event_compressed, receiver, event);
        return;
    }
    
  4. 將事件插入接收對象所線上程的post事件列表中,並喚醒線程的事件調度器,來進行事件的處理。所以postEvent是非阻塞的,因為其只是把事件插入了線程的事件列表,喚醒事件調度器之後便返回

        // delete the event on exceptions to protect against memory leaks till the event is
        // properly owned in the postEventList
        QScopedPointer<QEvent> eventDeleter(event);
        Q_TRACE(QCoreApplication_postEvent_event_posted, receiver, event, event->type());
        data->postEventList.addEvent(QPostEvent(receiver, event, priority));
        eventDeleter.take();
        event->posted = true;
        ++receiver->d_func()->postedEvents;
        data->canWait = false;
        locker.unlock();
    
        QAbstractEventDispatcher* dispatcher = data->eventDispatcher.loadAcquire();
        if (dispatcher)
            dispatcher->wakeUp();
    

事件是如何處理的?

在Qt中,事件的接收者都是QObject,而QObject中事件處理是調用event函數。如果當時對象不處理某個事件,就會將其轉發到父類的event進行處理。
而事件的處理,主要分為三個部分:

  1. 先是由事件迴圈遍歷事件
  2. 然後判斷事件接受對象有沒有安裝事件過濾器(installEventFilter),有安裝的話,就把事件丟給事件過濾器(eventFilter)進行處理。
  3. 如果沒有安裝事件過濾器或者事件過濾器對該事件不進行處理的話,那麼,事件將會進一步轉發到event函數里進行處理。

所以,在這一章節,我們同樣一步一步的分析這三個點。

事件迴圈是怎麼遍歷的?

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    MainWindow w;
    w.show();
    return a.exec();
}

上面是一個經典的QtGUI程式的main函數,調用a.exec()

int QCoreApplication::exec()
{
    ...
    
    threadData->quitNow = false;
    QEventLoop eventLoop;
    self->d_func()->in_exec = true;
    self->d_func()->aboutToQuitEmitted = false;
    int returnCode = eventLoop.exec();
    
    ...
}

而看QApplication::exec的源碼,實際上就是開啟了一個事件迴圈(QEventLoop)。同樣,我們去看QEventLoop::exec的源碼,進一步看處理事件的步驟是什麼。

int QEventLoop::exec(ProcessEventsFlags flags)
{
    ...

    while (!d->exit.loadAcquire())
        processEvents(flags | WaitForMoreEvents | EventLoopExec);

    ref.exceptionCaught = false;
    return d->returnCode.loadRelaxed();
}

上面可以看到,QEvenLoop::exec里,是一個while迴圈,迴圈的去調用processEvent,而且設置了WaitForMoreEvents就是說,如果沒有事件,就阻塞等待。

void QCoreApplication::processEvents(QEventLoop::ProcessEventsFlags flags, int ms)
{
    // ### Qt 6: consider splitting this method into a public and a private
    //           one, so that a user-invoked processEvents can be detected
    //           and handled properly.
    QThreadData *data = QThreadData::current();
    if (!data->hasEventDispatcher())
        return;
    QElapsedTimer start;
    start.start();
    while (data->eventDispatcher.loadRelaxed()->processEvents(flags & ~QEventLoop::WaitForMoreEvents)) {
        if (start.elapsed() > ms)
            break;
    }
}

閱讀processEvent,其調用了線程的事件調度器QAbstrctEventDispatcher,而這個類是一個抽象基類,根據不同的平臺,有不同的實現,我們以windows下(QEventDispatcherWin32)的為例,接著分析事件處理的流程。

bool QEventDispatcherWin32::processEvents(QEventLoop::ProcessEventsFlags flags)
{
    Q_D(QEventDispatcherWin32);

	...

    // To prevent livelocks, send posted events once per iteration.
    // QCoreApplication::sendPostedEvents() takes care about recursions.
    sendPostedEvents();

    ...
}

void QEventDispatcherWin32::sendPostedEvents()
{
    Q_D(QEventDispatcherWin32);

    if (d->sendPostedEventsTimerId != 0)
        KillTimer(d->internalHwnd, d->sendPostedEventsTimerId);
    d->sendPostedEventsTimerId = 0;

    // Allow posting WM_QT_SENDPOSTEDEVENTS message.
    d->wakeUps.storeRelaxed(0);

    QCoreApplicationPrivate::sendPostedEvents(0, 0, d->threadData.loadRelaxed());
}

可以看到,事件調度器最終還是調用了QCoreApplicationsendPostEvents

void QCoreApplicationPrivate::sendPostedEvents(QObject *receiver, int event_type,
                                               QThreadData *data)
{
    if (event_type == -1) {
        // we were called by an obsolete event dispatcher.
        event_type = 0;
    }

    if (receiver && receiver->d_func()->threadData != data) {
        qWarning("QCoreApplication::sendPostedEvents: Cannot send "
                 "posted events for objects in another thread");
        return;
    }

    ...

    // Exception-safe cleaning up without the need for a try/catch block
    struct CleanUp {
        QObject *receiver;
        int event_type;
        QThreadData *data;
        bool exceptionCaught;

        inline CleanUp(QObject *receiver, int event_type, QThreadData *data) :
            receiver(receiver), event_type(event_type), data(data), exceptionCaught(true)
        {}
        inline ~CleanUp()
        {
            if (exceptionCaught) {
                // since we were interrupted, we need another pass to make sure we clean everything up
                data->canWait = false;
            }

            --data->postEventList.recursion;
            if (!data->postEventList.recursion && !data->canWait && data->hasEventDispatcher())
                data->eventDispatcher.loadRelaxed()->wakeUp();

            // clear the global list, i.e. remove everything that was
            // delivered.
            if (!event_type && !receiver && data->postEventList.startOffset >= 0) {
                const QPostEventList::iterator it = data->postEventList.begin();
                data->postEventList.erase(it, it + data->postEventList.startOffset);
                data->postEventList.insertionOffset -= data->postEventList.startOffset;
                Q_ASSERT(data->postEventList.insertionOffset >= 0);
                data->postEventList.startOffset = 0;
            }
        }
    };
    CleanUp cleanup(receiver, event_type, data);

    while (i < data->postEventList.size()) {
       ...

        // first, we diddle the event so that we can deliver
        // it, and that no one will try to touch it later.
        pe.event->posted = false;
        QEvent *e = pe.event;
        QObject * r = pe.receiver;

        --r->d_func()->postedEvents;
        Q_ASSERT(r->d_func()->postedEvents >= 0);

        // next, update the data structure so that we're ready
        // for the next event.
        const_cast<QPostEvent &>(pe).event = nullptr;

        locker.unlock();
        const auto relocker = qScopeGuard([&locker] { locker.lock(); });

        QScopedPointer<QEvent> event_deleter(e); // will delete the event (with the mutex unlocked)

        // after all that work, it's time to deliver the event.
        QCoreApplication::sendEvent(r, e);

        // careful when adding anything below this point - the
        // sendEvent() call might invalidate any invariants this
        // function depends on.
    }

    cleanup.exceptionCaught = false;
}

我們一個一個的分塊分析:

  1. 判斷是否在一個線程

    if (receiver && receiver->d_func()->threadData != data) {
        qWarning("QCoreApplication::sendPostedEvents: Cannot send "
                 "posted events for objects in another thread");
        return;
    }
    
  2. 一個有意思的異常安全的處理,不需要try/catch塊

    // Exception-safe cleaning up without the need for a try/catch block
    struct CleanUp {
        QObject *receiver;
        int event_type;
        QThreadData *data;
        bool exceptionCaught;
    
        inline CleanUp(QObject *receiver, int event_type, QThreadData *data) :
            receiver(receiver), event_type(event_type), data(data), exceptionCaught(true)
        {}
        inline ~CleanUp()
        {
            if (exceptionCaught) {
                // since we were interrupted, we need another pass to make sure we clean everything up
                data->canWait = false;
            }
    
            --data->postEventList.recursion;
            if (!data->postEventList.recursion && !data->canWait && data->hasEventDispatcher())
                data->eventDispatcher.loadRelaxed()->wakeUp();
    
            // clear the global list, i.e. remove everything that was
            // delivered.
            if (!event_type && !receiver && data->postEventList.startOffset >= 0) {
                const QPostEventList::iterator it = data->postEventList.begin();
                data->postEventList.erase(it, it + data->postEventList.startOffset);
                data->postEventList.insertionOffset -= data->postEventList.startOffset;
                Q_ASSERT(data->postEventList.insertionOffset >= 0);
                data->postEventList.startOffset = 0;
            }
        }
    };
    CleanUp cleanup(receiver, event_type, data);
    

定義了一個結構體CleanUp,結構體的析構函數(~CleanUp)保存了函數退出時需要執行的清理操作。然後在棧上創建了一個結構體對象,遍歷事件列表時,異常退出,那麼就會調用自動調用~CleanUp的析構函數。

  1. 將事件發送出去(sendEvent)

    while (i < data->postEventList.size()) {
           ...
    
            // first, we diddle the event so that we can deliver
            // it, and that no one will try to touch it later.
            pe.event->posted = false;
            QEvent *e = pe.event;
            QObject * r = pe.receiver;
    
            --r->d_func()->postedEvents;
            Q_ASSERT(r->d_func()->postedEvents >= 0);
    
            // next, update the data structure so that we're ready
            // for the next event.
            const_cast<QPostEvent &>(pe).event = nullptr;
    
            locker.unlock();
            const auto relocker = qScopeGuard([&locker] { locker.lock(); });
    
            QScopedPointer<QEvent> event_deleter(e); // will delete the event (with the mutex unlocked)
    
            // after all that work, it's time to deliver the event.
            QCoreApplication::sendEvent(r, e);
    
            // careful when adding anything below this point - the
            // sendEvent() call might invalidate any invariants this
            // function depends on.
        }
    

可以看到,核心還是調用sendEvent將事件發送出去,而前面我們對sendEvent的源碼分析我們可以看到,事件先是經過事件過濾器,再經過對象的event函數,來進行事件的處理。所以就引出我們的下一個話題:事件過濾器

事件過濾器

在實際應用中,我們經常要將某一個視窗部件的某個事件如滑鼠滑輪滾動攔截,然後執行我們自己想要的操作。這個時候,我們就可以用到事件過濾器(EventFilter**) **
首先,我們需要自己編寫一個eventFilter函數,

bool Class::eventFilter(QObject* watcher, QEvent* event)
{
	//以過濾滑鼠滾輪事件為例
    if (object == m_watcherObject && event->type() == QEvent::Wheel) {
    	// do something
        return true;       
    }

    QWidget::eventFilter(watcher, event);
}

然後,我們需要為要攔截的某個視窗部件,安裝事件過濾器

void Class::initUI() 
{
	QWidget* m_watcherObject = new QWidget(this);
    // 為對象安裝一個事件過濾器
	m_watcherObject->installEventFilterr(this);
}

initUI();

那麼一個對象安裝的多個事件過濾器,會以什麼樣的順序觸發呢?我們在前面的講過,後安裝的事件過濾器會先觸發,這一點,我們可以在源碼里得到佐證:

void QObject::installEventFilter(QObject *obj)
{
    Q_D(QObject);
    if (!obj)
        return;
    if (d->threadData != obj->d_func()->threadData) {
        qWarning("QObject::installEventFilter(): Cannot filter events for objects in a different thread.");
        return;
    }

    if (!d->extraData)
        d->extraData = new QObjectPrivate::ExtraData;

    // clean up unused items in the list
    d->extraData->eventFilters.removeAll((QObject*)nullptr);
    d->extraData->eventFilters.removeAll(obj);
    d->extraData->eventFilters.prepend(obj);
}

可以清楚的看到,事件過濾器,是以prepend的形式被添加進事件過濾器列表的。
那麼,當有滑鼠滾輪事件觸發的時候,我們可以看到sendEvent會優先走到事件過濾器里,如果eventFilter返回一個true,那麼事件就不會被繼續派發,否則,將會將事件發送到其他的事件過濾器里進行處理,如果其他的事件過濾器均對該事件不進行處理,那麼事件將會繼續往下派發,走到事件的處理函數event

event

接下來,就到了事件處理的最後一站,event函數,這個函數比較簡單,我們可以自己重寫這個函數,對事件進行自定義的處理。

bool Class::event(QEvent *e)
{
    switch (e->type()) {
    case QEvent::Whell:
        // do something
        return true;

    default:
        if (e->type() >= QEvent::User) {
            customEvent(e);
            break;
        }
        return false;
    }
    return true;
}

夾帶私貨時間

  1. 之前有說到processEvent,添加一個小經驗。當我們有時候不得不在主線程迴圈執行很耗時的操作的時候,這個時候,界面就會刷新不過來,就會導致界面卡頓,影響使用。但是,我們可以在這個迴圈里,手動調用qApp->processEvent(),這樣就可以手動調用處理掉所有的事件,就可以解決卡頓的問題

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

-Advertisement-
Play Games
更多相關文章
  • 緒論 本文將介紹一個完全用Verilog HDL手寫的AMBA片上系統,項目的主題是設計一個基於AMBA匯流排的流水燈控制系統, 項目中所有數字電路邏輯都將通過Verilog進行RTL設計,不會調用成熟IP核, 然後利用Vivado平臺對RTL模型進行模擬、綜合與佈線,最後在FPGA開發板上進行板級驗 ...
  • 相信有過工作經驗的同學都知道資料庫連接是一個比較耗資源的操作。那麼資源到底是耗費在哪裡呢? 本文主要想探究一下連接資料庫的細節,尤其是在Web應用中要使用資料庫來連接池,以免每次發送一次請求就重新建立一次連接。對於這個問題,答案都是一致的,建立資料庫連接很耗時,但是這個耗時是都多少呢,又是分別在哪些 ...
  • L1-087 機工士姆斯塔迪奧 分數 20 全屏瀏覽題目 切換佈局 作者 DAI, Longao 單位 杭州百騰教育科技有限公司 在 MMORPG《最終幻想14》的副本“樂欲之所甌博訥修道院”里,BOSS 機工士姆斯塔迪奧將會接受玩家的挑戰。 你需要處理這個副本其中的一個機制:N×M 大小的地圖被拆 ...
  • 功能實現03 9.功能08-分頁顯示 9.1需求分析 將查詢的數據進行分頁顯示,要求功能如下: 顯示共多少條記錄 可以設置每頁顯示幾條 點擊第幾頁,顯示對應的數據 9.2思路分析 後端使用MyBatisPlus分頁插件完成查詢 修改FurnController,增加處理分頁顯示代碼 完成前臺代碼,加 ...
  • 官網:https://doc.cfd.direct/openfoam/user-guide-v9/platehole $FOAM_TUTORIALS/stressAnalysis/solidDisplacementFoam下的案例 1、網格劃分 /* *- C++ -* *\ | \\ / F ie ...
  • Spring Boot整合Google Bard - Web介面訪問Google AI聊天機器人 之前開發了一個關於Google Bard的Java庫,可以幫助我們簡單的提問並獲得答案。現在我把它整合到Spring Boot應用中,通過Web API讓大家可以訪問。 添加依賴 把pkslow goo ...
  • React Router 備忘清單 IT寶庫整理的React Router開發速查清單適合初學者的綜合 React Router 6.x 備忘清單入門,為開發人員分享快速參考備忘單。 開發速查表大綱 入門 安裝使用 添加路由器 根路由 處理未找到錯誤 contacts 用戶界面 嵌套路由 客戶端路由 ...
  • Redis 備忘清單 IT寶庫整理的Redis開發速查備忘清單 - 本備忘單旨在快速理解 redis 所涉及的主要概念,提供了最常用的SQL語句,供您參考。入門,為開發人員分享快速參考備忘單。 開發速查表大綱 入門 介紹 小試 數據類型 Redis服務相關的命令設置 COMMAND 一些引用(可能有 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...