Qt

qtbase
GammaRay
libQGLViewer
QtDirect3D
qrhiimgui
rhiwidget
QTWin11
QtFluentWin11
qtframework
qt-mvvm
SingleApplication
QtDocumentCN
QtWS16 -> cocoa
Qocoa
Qt-Nice-Frameless-Window
Qt-FramelessNativeWindow
QtCanvas
qtsingleapplication
qpdf
rxqt
qt-event-dispatcher-libuv
IcePlayer
qtpdfium
Log4Qt
qjson
QttpServer
libQGLViewer

article

Qt:工欲善其事,必先利其器。在Qt中使用Sentry在线收集日志
https://zhuanlan.zhihu.com/p/339169189

vcpkg build qtbase

1
2
3
4
5
6
7
8
9
qtbase 
M1 3min
16Pro 4Min
Parallels Arm64 7min

vcpkg qtbase debug
UTM arm64 Windows
x86_arm64 21min
arm64 12min

some note

qt5 -> qt6
QTextCodec -> QStringConverter

Qt的隐式共享和写时拷贝
https://0cch.cn/2021/07/11/qt-implicit-sharing-copy-on-write/

implicit sharing = cow + rc
禁止直接引用并通过引用修改Qt容器中的对象

d-pointer
pointer to implementation (pimpl)

s.detach

q-pointer

一文搞懂可重入和线程安全
https://blog.csdn.net/gaoyuelon/article/details/126672977
一个线程安全的函数可以被多个线程同时调用,即使调用使用共享数据,因为对共享数据的所有引用都是序列化的(串行的)
一个可重入函数也可以被多个线程同时调用,但前提是每个调用都使用自己的数据。
因此,线程安全的函数总是可重入的,但可重入的函数并不总是线程安全的。
c++类通常是可重入的,这是因为它们只访问自己的成员数据。任何线程都可以调用可重入类实例中的成员函数,只要没有其他线程可以同时调用该类的同一个实例上的成员函数。

线程安全与可重入
https://www.jianshu.com/p/4003b0f7fc6e
重入应该是指在单线程的情况下发生的,只有在发生中断、接收到信号的时候才会发生,因此可重入函数一般是对信号处理函数、中断处理函数的要求。

C++11单例模式
https://zhuanlan.zhihu.com/p/406287036
单例模式还需要进行拷贝控制呀,也就是显式地删除copy/move constructor 和 copy/move assignment operator
https://github.com/ltimaginea/Design-Patterns/blob/main/DesignPatterns/Singleton/Singleton.cpp

命令模式 Undo Framework
观察者模式 信号槽/事件过滤器
工厂模式 QTextCodec/QStyleFactory

deleteLater ->
QEvent::DeferredDelete ->
allowDeferredDelete ->
qDeleteInEventHandler ->
delete o;

QPointer只有一个作用QObject引用对象删除时自动置空
QPointer -> QWeakPointer -> QSharedPointer
std::weak_ptr -> expired() -> std::shared_ptr
QScopedPointer -> std::unique_ptr

注意,由于 moc 只处理头文件中的标记了Q_OBJECT的类声明,不会处理 cpp 文件中的类似声明。因此,如果我们的Newspaper和Reader类位于 main.cpp 中,是无法得到 moc 的处理的。解决方法是,我们手动调用 moc 工具处理 main.cpp,并且将 main.cpp 中的#include “newspaper.h”改为#include “moc_newspaper.h”就可以了。不过,这是相当繁琐的步骤,为了避免这样修改,我们还是将其放在头文件中。许多初学者会遇到莫名其妙的错误,一加上Q_OBJECT就出错,很大一部分是因为没有注意到这个宏应该放在头文件中。

信号作为函数名,不需要在 cpp 函数中添加任何实现(我们曾经说过,Qt 程序能够使用普通的 make 进行编译。没有实现的函数名怎么会通过编译?原因还是在 moc,moc 会帮我们实现信号函数所需要的函数体,所以说,moc 并不是单纯的将 Q_OBJECT 展开,而是做了很多额外的操作)

对于 Qt4 的信号槽连接语法,其连接是在运行时完成的,因此即便是 private 的槽函数也是可以作为槽进行连接的。但是,如果你使用了 Qt5 的新语法,新语法提供了编译期检查(取函数指针),因此取 private 函数的指针是不能通过编译的。

1
2
if (d->parent)        // remove it from parent object
d->setParent_helper(nullptr);

crash -> stack orderpp

1
2
3
4
5
6
{
QPushButton quit("Quit");
QWidget window;

quit.setParent(&window);
}

Qt5 connect 使用之“重载信号和槽”
https://zhuanlan.zhihu.com/p/367431938

继续使用 QPointer
https://www.devbean.net/2012/09/continue-using-qpointer/

Qt 使用QDialog::exec()实现应用程序级别的模态对话框,使用QDialog::open()实现窗口级别的模态对话框,使用QDialog::show()实现非模态对话框。

setAttribute()函数设置对话框关闭时,自动销毁对话框。另外,QObject还有一个deleteLater()函数,该函数会在当前事件循环结束时销毁该对话框(具体到这里,需要使用exec()开始一个新的事件循环)

不要担心如果对话框关闭,是不是还能获取到数据。因为 Qt 信号槽的机制保证,在槽函数在调用的时候,我们始终可以使用sender()函数获取到 signal 的发出者。

对于一个普通的QDialog而言,Qt 使用的是QDialogButtonBox这个类来实现不同平台的对话框按钮顺序的显示的。

我们不能在函数指针中使用函数参数的默认值。这是 C++ 语言的限制:参数默认值只能使用在直接地函数调用中。当使用函数指针取其地址的时候,默认参数是不可见的!
如果你还是想使用 Qt 5 的新语法,目前的办法只有一个:Lambda 表达式。

具体来说:如果一个事件处理函数调用了一个事件对象的accept()函数,这个事件就不会被继续传播给其父组件;如果它调用了事件的ignore()函数,Qt 会从其父组件中寻找另外的接受者。

在一个特殊的情形下,我们必须使用accept()和ignore()函数,那就是窗口关闭的事件。对于窗口关闭QCloseEvent事件,调用accept()意味着 Qt 会停止事件的传播,窗口关闭;调用ignore()则意味着事件继续传播,即阻止窗口关闭。

由此可以见,event()是一个集中处理不同类型的事件的地方。如果你不想重写一大堆事件处理器,就可以重写这个event()函数,通过QEvent::type()判断不同的事件。鉴于重写event()函数需要十分小心注意父类的同名函数的调用,一不留神就可能出现问题,所以一般还是建议只重写事件处理器(当然,也必须记得是不是应该调用父类的同名处理器)。这其实暗示了event()函数的另外一个作用:屏蔽掉某些不需要的事件处理器。

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

我们可以向一个对象上面安装多个事件处理器,只要调用多次installEventFilter()函数。如果一个对象存在多个事件过滤器,那么,最后一个安装的会第一个执行,也就是后进先执行的顺序。????
d->extraData->eventFilters.prepend(obj);

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

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

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

其中,QEvent::User的值是 1000,QEvent::MaxUser的值是 65535。从这里知道,我们最多可以定义 64536 个事件。通过这两个枚举值,我们可以保证我们自己的事件类型不会覆盖系统定义的事件类型。但是,这样并不能保证自定义事件相互之间不会被覆盖。为了解决这个问题,Qt 提供了一个函数:registerEventType(),用于自定义事件的注册。

sendPostedEvents
这个函数的作用是,将事件队列中的接受者为receiver,事件类似为 event_type 的所有事件立即发送给 receiver 进行处理。需要注意的是,来自窗口系统的事件并不由这个函数进行处理,而是processEvent()。

处理自定义事件,同前面我们讲解的那些处理方法没有什么区别。我们可以重写QObject::customEvent()函数,该函数接收一个QEvent对象作为参数。

QPainter -> QPaintEngine -> QPaintDevice

因为在实际设备上,像素是最小单位,我们不能像上面一样,在两个像素之间进行绘制。所以在实际绘制时,Qt 的定义是,绘制点所在像素是逻辑定义点的右下方的像素。

emit 是一个空的宏。甚至 MOC 也不会处理它。换句话说,emit 其实是可选的,没有什么含义(除了提醒开发者)。

在每一个 QMetaObject 中,槽、信号以及其它该对象可调用的函数都会分配一个从 0 开始的索引。它们是有顺序的,信号在第一位,然后是槽,最后是其它函数。这个索引在内部被称为相对索引。它们不包含父对象的索引位。

Graphics View 使用了 BSP 树(Binary Space Partitioning tree,这是一种被广泛应用于图形学方面的数据结构)来提供非常快速的元素发现,也正因为如此,才能够实现一种上百万数量级元素的实时显示机制。

需要注意的是,原子引用计数并不能保证线程安全,还是需要恰当的锁机制。这种观点对所有类似的场合都是适用的。原子引用计数能够保证的是,线程肯定操作自己的数据,线程自己的数据是安全的。

WebChannelTransport::Install

https://github.com/qt/qtwebengine/blob/dev/src/core/renderer/web_channel_ipc_transport.cpp

QHttpThreadDelegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Move the delegate to the http thread
delegate->moveToThread(thread);

void QNetworkReplyHttpImplPrivate::postRequest(const QNetworkRequest &newHttpRequest)
{
Q_Q(QNetworkReplyHttpImpl);

QThread *thread = nullptr;
if (synchronous) {
// A synchronous HTTP request uses its own thread
thread = new QThread();
thread->setObjectName(QStringLiteral("Qt HTTP synchronous thread"));
QObject::connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
thread->start();
} else {
// We use the manager-global thread.
// At some point we could switch to having multiple threads if it makes sense.
thread = managerPrivate->createThread();
}
// ...
}

PTHREAD_CREATE_DETACHED/CREATE_SUSPENDED(WIN32)

QThread -> start/exec(run) -> detach/join

如果真的有事件被移动,则尝试对目标线程调用wakeUp,告诉线程可以起来工作了

QThreadPrivate::start ->
thr->run(); ->
QThread::exec ->
QEventLoop -> exec

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

QThreadPrivate::createEventDispatcher
QEventDispatcherWin32
d->internalHwnd = qt_create_internal_window(this);
unix eventfd/pipe

在登录时,我们必须要等待网络返回结果,才能让界面做出响应:是验证成功进入系统,还是验证失败做出提示?这就是本章的主要内容:如何使用QNetworkAccessManager进行同步网络访问。

https://www.devbean.net/2013/11/qt-study-road-2-thread-and-event-loop/
伪代码里面的while会遍历整个事件队列,发送从队列中找到的事件;wait_for_more_events()函数则会阻塞事件循环,直到又有新的事件产生。我们仔细考虑这段代码,在wait_for_more_events()函数所得到的新的事件都应该是由程序外部产生的。因为所有内部事件都应该在事件队列中处理完毕了。因此,我们说事件循环在wait_for_more_events()函数进入休眠,并且可以被下面几种情况唤醒:

窗口管理器的动作(键盘、鼠标按键按下、与窗口交互等);
套接字动作(网络传来可读的数据,或者是套接字非阻塞写等);
定时器;
由其它线程发出的事件(我们会在后文详细解释这种情况)。

当然,这种情况也有解决的办法:我们可以在调用QCoreApplication::processEvents()函数时传入QEventLoop::ExcludeUserInputEvents参数,意思是不要再次派发用户输入事件(这些事件仍旧会保留在事件队列中)。

值得注意的一点是,虽然QObject是可重入的,但是 GUI 类,特别是QWidget及其所有的子类,都是不是可重入的。它们只能在主线程使用。由于这些 GUI 类大都需要一个事件循环,所以,调用QCoreApplication::exec()也必须是主线程,否则这些 GUI 类就没有事件循环了。

基于同样的原因,你也不能在另外的线程直接delete一个QObject对象,相反,你需要调用QObject::deleteLater()函数,这个函数会给对象所在线程发送一个删除的事件。

假设我们有一个很耗时的计算,我们不能简单地将它移动到另外的线程(或者是我们根本无法移动它,比如这个任务必须在 GUI 线程完成)。如果我们将这个计算任务分割成小块,那么我们就可以及时返回事件循环,从而让事件循环继续派发事件,调用处理下一个小块的函数。

https://www.devbean.net/2012/08/qt-study-road-2-catelog/

QEventDispatcher
cf -> CFRunLoopWakeup
glib -> g_main_context_wakeup

QEventDispatcherGlib::processEvents
1
g_main_context_iteration

unix -> d->threadPipe.wakeUp() -> eventfd/qt_safe_write[pipe]

processEvents
1
2
3
4
5
6
7
qt_safe_poll ->
d->threadPipe.check ->
eventfd_read/read
// include_notifiers
d->activateSocketNotifiers()
// include_timers
d->activateTimers()

wasm -> g_proxyingQueue.proxyAsync [emscripten::ProxyingQueue]
win -> PostMessage[WM_QT_SENDPOSTEDEVENTS]

1
2
3
4
if (HIWORD(GetQueueStatus(mask)) == 0)
q->sendPostedEvents();
else
d->startPostedEventsTimer();

QFuture -> whenAllImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// checkForCompletion
if (!ready.fetchAndStoreRelaxed(true)) {
promise.addResult(std::forward<T>(result));
promise.finish();
}
//
void reportFinished()
{
`QFutureInterfaceBase::reportFinished();
QFutureInterfaceBase::runContinuation();`
}
// runContinuation ->
auto fn = std::move(d->continuation);
lock.unlock();
fn(*this);
lock.relock();

when完成时wakeup 通知不需要等待 然后执行d->continuation
https://github.com/qt/qtbase/blob/dev/src/corelib/thread/qfuture_impl.h
WhenAllContext/WhenAnyContext -> checkForCompletion

Qt Invoke

QMetaMethodInvoker::invokeImpl

QEventDispatcher
win32 -> PostMessage -> WM_QT_SENDPOSTEDEVENTS
sendPostedEvents/startPostedEventsTimer
unix -> threadPipe.wakeUp
eventfd or pipe/pipe2
mac ->

postEvent -> sendPostedEvent ->
sendEvent -> notifyInternal2 -> notify_helper
sendThroughApplicationEventFilters/
receiver->event(event);

connectNotify
QObjectPrivate::get(s)->addConnection

signal_func ->
QMetaObject:activate ->
doActivate ->
connections
isSlotObject -> obj->call
callFunction -> callFunction
QMetaObject::metacall

QEvent::KeyRelease -> QWindowSystemInterface::handleExtendedKeyEvent
QWindowsContext::windowsProc -> QtWindows::AppCommandEvent

macOS用vcpkg编译qtbase动态库报错处理

1
vcpkg install qtbase --triplet arm64-osx-dynamic
icu_qt_dynamic.patch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
diff --git a/ports/icu/portfile.cmake b/ports/icu/portfile.cmake
index c622dfd..350cca2 100644
--- a/ports/icu/portfile.cmake
+++ b/ports/icu/portfile.cmake
@@ -91,7 +91,7 @@ if(VCPKG_TARGET_IS_OSX AND VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic")
#31680: Fix @rpath in both debug and release build
foreach(CONFIG_TRIPLE IN ITEMS ${DEBUG_TRIPLET} ${RELEASE_TRIPLET})
# add ID_PREFIX to libicudata libicui18n libicuio libicutu libicuuc
- foreach(LIB_NAME IN ITEMS libicudata libicui18n libicuio ${LIBICUTU_RPATH} libicuuc)
+ foreach(LIB_NAME IN ITEMS libicui18n libicuio libicuuc)
vcpkg_execute_build_process(
COMMAND "${INSTALL_NAME_TOOL}" -id "${ID_PREFIX}/${LIB_NAME}.${ICU_VERSION_MAJOR}.dylib"
"${LIB_NAME}.${VERSION}.dylib"
@@ -99,6 +99,13 @@ if(VCPKG_TARGET_IS_OSX AND VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic")
LOGNAME "make-build-fix-rpath-${CONFIG_TRIPLE}"
)
endforeach()
+ set(LIB_NAME libicudata)
+ vcpkg_execute_build_process(
+ COMMAND "${INSTALL_NAME_TOOL}" -id "${ID_PREFIX}/${LIB_NAME}.${ICU_VERSION_MAJOR}.dylib"
+ "${LIB_NAME}.${VERSION}.dylib"
+ WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${CONFIG_TRIPLE}/stubdata"
+ LOGNAME "make-build-fix-rpath-${CONFIG_TRIPLE}"
+ )

# add ID_PREFIX to libicui18n libicuio libicutu dependencies
foreach(LIB_NAME IN ITEMS libicui18n libicuio)
@@ -119,7 +126,7 @@ if(VCPKG_TARGET_IS_OSX AND VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic")
endforeach()

# add ID_PREFIX to remaining libicuio libicutu dependencies
- foreach(LIB_NAME libicuio libicutu)
+ foreach(LIB_NAME libicuio)
vcpkg_execute_build_process(
COMMAND "${INSTALL_NAME_TOOL}" -change "libicui18n.${ICU_VERSION_MAJOR}.dylib"
"${ID_PREFIX}/libicui18n.${ICU_VERSION_MAJOR}.dylib"

${HOMEBREW_PREFIX}/share/xmake/rules/qt/deploy/macosx.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
diff --git a/xmake/rules/qt/deploy/macosx.lua b/xmake/rules/qt/deploy/macosx.lua
index 5abbe4263..066a3dea3 100644
--- a/xmake/rules/qt/deploy/macosx.lua
+++ b/xmake/rules/qt/deploy/macosx.lua
@@ -91,7 +91,7 @@ function main(target, opt)
local qt = assert(find_qt(), "Qt SDK not found!")

-- get macdeployqt
- local macdeployqt = path.join(qt.bindir, "macdeployqt")
+ local macdeployqt = path.join(qt.libexecdir, "macdeployqt")
assert(os.isexec(macdeployqt), "macdeployqt not found!")

-- generate target app

1
2
3
4
export VCPKG_ROOT=/Users/wurui/Documents/GitHub/vcpkg
export PATH=${VCPKG_ROOT}/installed/arm64-osx-dynamic/tools/Qt6/bin:$PATH
export DYLD_LIBRARY_PATH=${VCPKG_ROOT}/installed/arm64-osx-dynamic/lib
xmake -yv