QGuiApplication底层鼠标处理(一)使用QSocketNotifier建立侦听连接
读取外设信息
鼠标、键盘、触屏等外部输入设备是以Plugin的方式加载的。
在QGuiApplication初始化时从argv和环境变量QT_QPA_GENERIC_PLUGINS读取所有的插件信息,里面就包括鼠标、键盘等。
void QGuiApplicationPrivate::init()
{
...
QList<QByteArray> pluginList;
...
for (int i=1; i<argc; i++) {
...
if (strcmp(arg, "-plugin") == 0) {
if (++i < argc)
pluginList << argv[i];
...
QByteArray envPlugins = qgetenv("QT_QPA_GENERIC_PLUGINS");
if (!envPlugins.isEmpty())
pluginList += envPlugins.split(',');
...
init_plugins(pluginList);
...
}
建立外设连接
init_plugins
获取到设备列表后,调用init_plugins来初始化所有插件。
static void init_plugins(const QList<QByteArray> &pluginList)
{
for (int i = 0; i < pluginList.count(); ++i) {
QByteArray pluginSpec = pluginList.at(i);
int colonPos = pluginSpec.indexOf(':');
QObject *plugin;
if (colonPos < 0)
plugin = QGenericPluginFactory::create(QLatin1String(pluginSpec), QString());
else
plugin = QGenericPluginFactory::create(QLatin1String(pluginSpec.mid(0, colonPos)),
QLatin1String(pluginSpec.mid(colonPos+1)));
if (plugin)
QGuiApplicationPrivate::generic_plugin_list.append(plugin);
else
qWarning("No such plugin for spec \"%s\"", pluginSpec.constData());
}
}
其中干活的是
QGenericPluginFactory::create
,不过这个函数也不是根据设备信息来创建逻辑设备的:
QObject *QGenericPluginFactory::create(const QString& key, const QString &specification)
{
return qLoadPlugin<QObject, QGenericPlugin>(loader(), key.toLower(), specification);
}
来看下qLoadPlugin这个模板函数:
template <class PluginInterface, class FactoryInterface, typename ...Args>
PluginInterface *qLoadPlugin(const QFactoryLoader *loader, const QString &key, Args &&...args)
{
const int index = loader->indexOf(key);
if (index != -1) {
QObject *factoryObject = loader->instance(index);
if (FactoryInterface *factory = qobject_cast<FactoryInterface *>(factoryObject))
if (PluginInterface *result = factory->create(key, std::forward<Args>(args)...))
return result;
}
return nullptr;
}
把模板参数填上,关键语句就是:
const int index = loader()->indexof(key);
QGenericPlugin * factory = loader()->instance(key);
QObject *result = factory->create(key, specification);
QEvdevMousePlugin
Qt内置了好几个QGenericPlugin,比如:QEvdevMousePlugin、QEvdevKeyboardPlugin、QEvdevTouchScreenPlugin等,从名称可以看出来他们是干啥的了。
QEvdevMousePlugin::create()
创建鼠标设备:
QObject* QEvdevMousePlugin::create(const QString &key,
const QString &specification)
{
if (!key.compare(QLatin1String("EvdevMouse"), Qt::CaseInsensitive))
return new QEvdevMouseManager(key, specification);
return 0;
}
QEvdevMouseManager
创建了QEvdevMouseManager,这个类很重要,它负责管理所有的鼠标设备,包括热插拔的,我们看他的构造函数,构造函数比较长,我们分三段看,第一段,添加当前设备,先通过spec参数解析,解析的结果为空再启用设备发现:
QEvdevMouseManager::QEvdevMouseManager(const QString &key, const QString &specification, QObject *parent)
: QObject(parent), m_x(0), m_y(0), m_xoffset(0), m_yoffset(0)
{
...
auto parsed = QEvdevUtil::parseSpecification(spec);
...
for (const QString &device : qAsConst(parsed.devices))
addMouse(device);
if (parsed.devices.isEmpty()) {
if (auto deviceDiscovery = QDeviceDiscovery::create(QDeviceDiscovery::Device_Mouse | QDeviceDiscovery::Device_Touchpad, this)) {
// scan and add already connected keyboards
const QStringList devices = deviceDiscovery->scanConnectedDevices();
for (const QString &device : devices)
addMouse(device);
第二段,处理设备热插拔,发现新设备插入调用addMouse添加,发现设备移除调用removeMouse:
connect(deviceDiscovery, &QDeviceDiscovery::deviceDetected,
this, &QEvdevMouseManager::addMouse);
connect(deviceDiscovery, &QDeviceDiscovery::deviceRemoved,
this, &QEvdevMouseManager::removeMouse);
}
}
第三段,处理光标:
QInputDeviceManager *manager = QGuiApplicationPrivate::inputDeviceManager();
connect(manager, &QInputDeviceManager::cursorPositionChangeRequested, [this](const QPoint &pos) {
m_x = pos.x();
m_y = pos.y();
clampPosition();
});
}
接下来重点看,拿到设备后如何关联到qt中,来看addMouse:
void QEvdevMouseManager::addMouse(const QString &deviceNode)
{
auto handler = QEvdevMouseHandler::create(deviceNode, m_spec);
if (handler) {
connect(handler.get(), &QEvdevMouseHandler::handleMouseEvent,
this, &QEvdevMouseManager::handleMouseEvent);
connect(handler.get(), &QEvdevMouseHandler::handleWheelEvent,
this, &QEvdevMouseManager::handleWheelEvent);
m_mice.add(deviceNode, std::move(handler));
updateDeviceCount();
}
}
这里创建了MouseHandler,并连接了handleMouseEvent来处理鼠标事件,连接了handleWheelEvent来处理滚轮事件。
QEvdevMouseHandler
先看下QEvdevMouseHandler::create:
std::unique_ptr<QEvdevMouseHandler> QEvdevMouseHandler::create(const QString &device, const QString &specification)
{
...
int fd = qt_safe_open(device.toLocal8Bit().constData(), O_RDONLY | O_NDELAY, 0);
if (fd >= 0) {
::ioctl(fd, EVIOCGRAB, grab);
return std::unique_ptr<QEvdevMouseHandler>(new QEvdevMouseHandler(device, fd, abs, compression, jitterLimit));
}
}
QEvdevMouseHandler::create打开了设备,将文件描述符传给了QEvdevMouseHandler的构造函数。
看下QEvdevMouseHandler构造函数:
QEvdevMouseHandler::QEvdevMouseHandler(const QString &device, int fd...)
: m_device(device), m_fd(fd)...
{
...
m_notify = new QSocketNotifier(m_fd, QSocketNotifier::Read, this);
connect(m_notify, &QSocketNotifier::activated,
this, &QEvdevMouseHandler::readMouseData);
}
快要接近真相了,这里创建了QSocketNotifier来监听打开的鼠标设备并连接了activated信号,当设备有数据可读时,就会发出activated信号,接着就到readMouseData,这个槽函数比较长,感兴趣的小伙伴可以去看下,这里贴上部分代码:
void QEvdevMouseHandler::readMouseData()
{
struct ::input_event buffer[32];
int result = QT_READ(m_fd, reinterpret_cast<char *>(buffer) + n, sizeof(buffer) - n);
for (int i = 0; i < n; ++i) {
struct ::input_event *data = &buffer[i];
if (data->type == EV_ABS) {
} else if (data->type == EV_REL) {
if (data->code == REL_X) {
....
} else if (data->code == ABS_WHEEL) { // vertical scroll
delta.setY(120 * data->value);
emit handleWheelEvent(delta);
} else if (data->code == ABS_THROTTLE) { // horizontal scroll
...
}
} else if(data->type == EV_KEY && data->code >= BTN_LEFT && data->code <= BTN_JOYSTICK) {
Qt::MouseButton button = Qt::NoButton;
...
switch (data->code) {
case 0x110: button = Qt::LeftButton; break; // BTN_LEFT
...
sendMouseEvent();
}
上面基本上就是最底层的处理了,读取鼠标设备的数据,分析数据得到坐标和状态,再发出鼠标事件信号和滚轮事件信号。
到这一步,鼠标数据的处理已经看到了,但是鼠标数据如何转化为QMouseEvent的呢?还记得前面
connect(handler.get(), &QEvdevMouseHandler::handleMouseEvent, this, &QEvdevMouseManager::handleMouseEvent);
QEvdevMouseHandler的handleMouseEvent如下:
void QEvdevMouseManager::handleMouseEvent(int x, int y, bool abs, Qt::MouseButtons buttons,
Qt::MouseButton button, QEvent::Type type)
{
...
QWindowSystemInterface::handleMouseEvent(0, pos, pos, buttons, button, type, QGuiApplicationPrivate::inputDeviceManager()->keyboardModifiers());
}
调用了QWindowSystemInterface::handleMouseEvent:
QWindowSystemInterface
QT_DEFINE_QPA_EVENT_HANDLER(void, handleMouseEvent, QWindow *window, ulong timestamp,
const QPointF &local, const QPointF &global, Qt::MouseButtons state,
Qt::MouseButton button, QEvent::Type type, Qt::KeyboardModifiers mods,
Qt::MouseEventSource source)
{
Q_ASSERT_X(type != QEvent::MouseButtonDblClick && type != QEvent::NonClientAreaMouseButtonDblClick,
"QWindowSystemInterface::handleMouseEvent",
"QTBUG-71263: Native double clicks are not implemented.");
auto localPos = QHighDpi::fromNativeLocalPosition(local, window);
auto globalPos = QHighDpi::fromNativePixels(global, window);
QWindowSystemInterfacePrivate::MouseEvent *e =
new QWindowSystemInterfacePrivate::MouseEvent(window, timestamp, localPos, globalPos,
state, mods, button, type, source);
QWindowSystemInterfacePrivate::handleWindowSystemEvent<Delivery>(e);
}
根据入参传入的鼠标数据信息构造了QMouseEvent,然后通过handleWindowSystemEvent进行处理,根据同步或异步来决定是直接处理还是放到消息队列。
总结
到这里结束,概括起来就是,将设备的文件描述符传给QSocketNotifier来侦听设备,有数据时读取数据,再分析数据得到鼠标坐标和状态,发出相应的信号,最后由handleWindowSystemEvent来将鼠标事件直接处理或者放入消息队列。