Qt跨平台窗口选择功能的实现过程

 更新时间:2022年12月09日 16:38:22   作者:mahuifa  
很多时候为了方便软件的使用,我们需要让编写的界面程序显示在最上层,这时候就需要对窗口属性进行调整,下面这篇文章主要给大家介绍了关于Qt跨平台窗口选择功能的实现过程,需要的朋友可以参考下

1、概述

  • Qt版本:V5.12.5
  • 兼容系统:
    • Windows:这里测试了Windows10,其它的版本没有测试;
    • Linux:这里测试了ubuntu18.04、20.04,其它的没有测试(ubuntu自带的截图软件没有这个功能);
    • Mac:等啥时候我有了Mac电脑再说。
  1. 我们在使用截图软件、录屏软件时常常有一个选项,就是窗口截图,当我们鼠标移动到窗口上时,程序会自动识别到鼠标位置的窗口,获取窗口的大小、位置,这是怎么实现的呢;
  2. 这里就研究了一下,如果使用Qt的鼠标事件、事件过滤器,一般鼠标出了窗口范围就不会触发鼠标事件了,想要获取全局鼠标事件只能使用系统API,Windows下就是使用user32、ubuntu下就是X11;
  3. 在这个示例中实现了自动获取鼠标所在坐标窗口的位置、大小信息,并使用一个矩形窗口框选、覆盖住所在窗口;
  4. 在windows实现的窗口选择功能可精确识别系统任务栏、程序图标、文件资源管理器的各个窗口部件等(很多截屏软件都没这个功能)。

2、实现效果

Windows下实现效果

Linux下实现效果

3、实现原理

Windows

  1. 使用定时器每隔200ms获取一次当前鼠标位置;
  2. 调用系统user32 API通过鼠标位置查询当前位置的窗口句柄;
  3. 通过获取的窗口句柄获取窗口的位置、大小;
  4. 将当前窗口覆盖到鼠标所在窗口上方(注意:当前窗口需要设置鼠标穿透,否则第2部获取到的就是当前窗口的句柄);

Linux

  1. 使用定时器每隔200ms获取一次当前鼠标位置;
  2. 调用系统x11 API获取当前屏幕的所有窗口;
  3. 通过获取的窗口句柄获取窗口的位置、大小;
  4. 通过当前窗口的句柄过滤掉获取的所有窗口句柄中的当前窗口(否则当前窗口因为是在最上层,每次获取的都是当前窗口的大小);
  5. 遍历所有窗口的位置、大小,判断包含鼠标位置的窗口,并记录位置、大小信息,由于遍历是从最底层窗口到最顶层窗口,所以需要保存最后一个窗口的位置、大小信息;
  6. 将当前窗口覆盖到鼠标所在窗口上方(注意:linux下不能设置鼠标穿透,否则窗口出现显示不全的问题);

4、关键代码

由于使用到了系统API,所以pro文件中需要链接系统库

win32 {
LIBS+= -luser32    # 使用WindowsAPI需要链接库
}
unix:!macx{
LIBS += -lX11      # linux获取窗口信息需要用到xlib
}

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTimer>

class Widget : public QWidget
{
    Q_OBJECT

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

protected:
    void on_timeout();
private:
    QTimer m_timer;
};
#endif // WIDGET_H

NtpClient.cpp

#include "widget.h"
#include <QDebug>
#include <qgridlayout.h>

#if defined(Q_OS_WIN)
#include <Windows.h>
#include <windef.h>
#elif defined(Q_OS_LINUX)
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#elif defined(Q_OS_MAC)
#endif

#if defined(Q_OS_WIN)
static HHOOK g_hook = nullptr;
/**
 * @brief           处理鼠标事件的回调函数
 * @param nCode
 * @param wParam
 * @param lParam
 * @return
 */
LRESULT CALLBACK CallBackProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    switch (wParam)
    {
    case WM_LBUTTONDOWN:   // 鼠标左键按下
    {
        POINT pos;
        bool ret = GetCursorPos(&pos);
        if(ret)
        {
            qDebug() << pos.x <<" " << pos.y;
        }
        qDebug() << "鼠标左键按下";
        break;
    }
    default:
        break;
    }
    return CallNextHookEx(nullptr, nCode, wParam, lParam);   // 注意这一行一定不能少,否则会出大问题
}
#endif


Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    this->setWindowTitle(QString("Qt-框选鼠标当前位置窗口范围 - V%1").arg(APP_VERSION));
#if defined(Q_OS_WIN)
    // linux下鼠标穿透要放在后面两行代码的全前面,否则无效(但是鼠标穿透了会导致一些奇怪的问题,如窗口显示不全,所以这里不使用)
    // windows下如果不设置鼠标穿透则只能捕获到当前窗口
    this->setAttribute(Qt::WA_TransparentForMouseEvents, true);
#endif
    this->setWindowFlags(Qt::FramelessWindowHint);                            // 去掉边框、标题栏
    this->setAttribute(Qt::WA_TranslucentBackground);                         // 背景透明
    this->setWindowFlags(this->windowFlags() | Qt::WindowStaysOnTopHint);     // 设置顶级窗口,防止遮挡

#if defined(Q_OS_WIN)
    // 由于windows不透明的窗体如果不设置设置鼠标穿透WindowFromPoint只能捕捉到当前窗体,而设置鼠标穿透后想要获取鼠标事件只能通过鼠标钩子
    g_hook = SetWindowsHookExW(WH_MOUSE_LL, CallBackProc, GetModuleHandleW(nullptr), 0);  // 挂载全局鼠标钩子
    if (g_hook)
    {
        qDebug() << "鼠标钩子挂接成功,线程ID:" << GetCurrentThreadId();

    }
    else
    {
        qDebug() << "鼠标钩子挂接失败:" << GetLastError();
    }
#endif

    // 在当前窗口上增加一层QWidget,否则不会显示边框
    QGridLayout* gridLayout = new QGridLayout(this);
    gridLayout->setSpacing(0);
    gridLayout->setContentsMargins(0, 0, 0, 0);
    gridLayout->addWidget(new QWidget(), 0, 0, 1, 1);

    this->setStyleSheet(" background-color: rgba(58, 196, 255, 40); border: 2px solid rgba(58, 196, 255, 200);"); // 设置窗口边框样式 dashed虚线,solid 实线

    // 使用定时器定时获取当前鼠标位置的窗口位置信息
    connect(&m_timer, &QTimer::timeout, this, &Widget::on_timeout);
    m_timer.start(200);

}

Widget::~Widget()
{
#if defined(Q_OS_WIN)
    if(g_hook)
    {
        bool ret = UnhookWindowsHookEx(g_hook);
        if(ret)
        {
            qDebug() << "卸载鼠标钩子。";
        }
    }
#endif
}

void Widget::on_timeout()
{
    QPoint point = QCursor::pos();  // 获取鼠标当前位置
#if defined(Q_OS_WIN)
    POINT pos;
    pos.x = point.x();
    pos.y = point.y();

    HWND hwnd = nullptr;
    hwnd = WindowFromPoint(pos);   // 通过鼠标位置获取窗口句柄
    if(!hwnd) return;

    RECT lrect;
    bool ret = GetWindowRect(hwnd, &lrect);     //获取窗口位置
    if(!ret) return;

    QRect rect;
    rect.setX(lrect.left);
    rect.setY(lrect.top);
    rect.setWidth(lrect.right - lrect.left);
    rect.setHeight(lrect.bottom - lrect.top);
    this->setGeometry(rect);         // 设置窗口边框
#elif defined(Q_OS_LINUX)  // linux下使用x11获取的窗口大小有可能不太准确,例如浏览器的大小会偏小
    // 获取根窗口
    Display* display = XOpenDisplay(nullptr);
    Window rootWindow = DefaultRootWindow(display);

    Window root_return, parent_return;
    Window * children = nullptr;
    unsigned int nchildren = 0;
    // 函数详细说明见xlib文档:https://tronche.com/gui/x/xlib/window-information/XQueryTree.html
    // 该函数会返回父窗口的子窗口列表children,因为这里用的是当前桌面的根窗口作为父窗口,所以会返回所有子窗口
    // 注意:窗口顺序(z-order)为自底向上
    XQueryTree(display, rootWindow, &root_return, &parent_return, &children, &nchildren);

    QRect recte;                      // 保存鼠标当前所在窗口的范围
    for(unsigned int i = 0; i < nchildren; ++i)
    {
        if(children[i] == this->winId()) continue;           // 由于当前窗口一直在最顶层,所以这里要过滤掉当前窗口,否则一直获取到的就是当前窗口大小
        XWindowAttributes attrs;
        XGetWindowAttributes(display, children[i], &attrs);  // 获取窗口参数
        if (attrs.map_state == IsViewable)                   // 只处理可见的窗口, 三个状态:IsUnmapped, IsUnviewable, IsViewable
        {
#if 0
            QRect rect(attrs.x + 1, attrs.y, attrs.width, attrs.height);  // 这里x+1防止全屏显示,如果不+1,设置的大小等于屏幕大小是会自动切换成全屏显示状态,后面就无法缩小了
#else
            QRect rect(attrs.x, attrs.y, attrs.width, attrs.height);
#endif
            if(rect.contains(point))                        // 判断鼠标坐标是否在窗口范围内
            {
                recte = rect;                               // 记录最后一个窗口的范围
            }
        }
    }
#if 0  // 在linux下使用setGeometry设置窗口会有一些问题
    this->showNormal();               // 第一次显示是如果是屏幕大小,则后面无法缩小,这里需要设置还原
    this->setGeometry(recte);         // 设置窗口边框
#else  // 使用setFixedSize+move可以避免这些问题
    this->move(recte.x(), recte.y());
    this->setFixedSize(recte.width(), recte.height());
#endif
//    qDebug() << this->rect() <<recte<< this->windowState();

    // 注意释放资源
    XFree(children);
    XCloseDisplay(display);

#elif defined(Q_OS_MAC)
#endif
}

5、源代码

总结

到此这篇关于Qt跨平台窗口选择功能实现的文章就介绍到这了,更多相关Qt跨平台窗口选择功能内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解C++设计模式编程中对状态模式的运用

    详解C++设计模式编程中对状态模式的运用

    这篇文章主要介绍了C++设计模式编程中对状态模式的运用,状态模式允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类,需要的朋友可以参考下
    2016-03-03
  • C++算法系列之日历生成的算法代码

    C++算法系列之日历生成的算法代码

    日历算法首先要知道日历的编排规则,也就是历法。所谓历法,指的就是推算年、月、日的时间长度和它们之间的关系,指定时间序列的法则。
    2018-05-05
  • C++编写高性能服务器实例教程

    C++编写高性能服务器实例教程

    这篇文章主要介绍了如何用C++编写高性能服务器,文中通过示例代码介绍的非常详细,对大家学习C++有一定的参考价值,需要的朋友们可以了解下
    2020-06-06
  • C++读写.mat文件的方法

    C++读写.mat文件的方法

    本文介绍了“C++读写.mat文件的方法”,需要的朋友可以参考一下
    2013-03-03
  • Windows 环境下使用 Qt 连接 MySQL

    Windows 环境下使用 Qt 连接 MySQL

    这篇文章主要介绍了Windows 环境下使用 Qt 连接 MySQL的相关资料,需要的朋友可以参考下
    2017-07-07
  • C++ OpenCV实战之图像透视矫正

    C++ OpenCV实战之图像透视矫正

    这篇文章主要介绍了通过C++ OpenCV实现图像的透视矫正,文中的示例代码讲解详细,对我们的学习或工作有一定的参考价值,感兴趣的可以了解一下
    2022-01-01
  • C++ 测试框架GoogleTest入门介绍

    C++ 测试框架GoogleTest入门介绍

    这篇文章主要为大家介绍了C++测试框架GoogleTest入门基础,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04
  • 使用C语言编写钢琴小程序

    使用C语言编写钢琴小程序

    这篇文章主要为大家详细介绍了使用C语言编写钢琴小程序,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-02-02
  • C语言中的数据类型强制转换

    C语言中的数据类型强制转换

    这篇文章主要介绍了C语言中的数据类型强制转换方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-03-03
  • C++读取wav文件中的PCM数据

    C++读取wav文件中的PCM数据

    这篇文章主要为大家详细介绍了C++读取wav文件中的PCM数据,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01

最新评论