QGuiApplication底层鼠标处理(一)使用QSocketNotifier建立侦听连接

  • Post author:
  • Post category:其他




读取外设信息

鼠标、键盘、触屏等外部输入设备是以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来将鼠标事件直接处理或者放入消息队列。



版权声明:本文为weixin_42981623原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。