From 1195d65c57169af56498747eb733f4f711359998 Mon Sep 17 00:00:00 2001 From: mhf <1603291350@qq.com> Date: Sat, 19 Nov 2022 00:13:45 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=AE=8C=E6=88=90=E6=88=AA?= =?UTF-8?q?=E5=9B=BE=E5=B7=A5=E5=85=B7=E5=8A=9F=E8=83=BD=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、完成窗口截图功能开发; 2、完成linux系统适配。 --- .../SnippingTool/SnippingTool.pro | 10 +- FunctionalModule/SnippingTool/mainwindow.cpp | 28 ++- FunctionalModule/SnippingTool/mainwindow.h | 2 + FunctionalModule/SnippingTool/windowrect.cpp | 216 ++++++++++++++++++ FunctionalModule/SnippingTool/windowrect.h | 58 +++++ 5 files changed, 305 insertions(+), 9 deletions(-) create mode 100644 FunctionalModule/SnippingTool/windowrect.cpp create mode 100644 FunctionalModule/SnippingTool/windowrect.h diff --git a/FunctionalModule/SnippingTool/SnippingTool.pro b/FunctionalModule/SnippingTool/SnippingTool.pro index 705b4d1..54390d8 100644 --- a/FunctionalModule/SnippingTool/SnippingTool.pro +++ b/FunctionalModule/SnippingTool/SnippingTool.pro @@ -1,5 +1,9 @@ #--------------------------------------------------------------------------------------- # @功能: Qt实现截图工具 +# 1、实现Windows、linux系统下截图功能; +# 2、实现全屏截图、矩形截图、窗口截图功能; +# 3、实现保存截图、取消截图功能; +# 4、实时显示截取的图片。 # @编译器: Desktop Qt 5.12.5 MSVC2017 64bit(也支持其它编译器) # @Qt IDE: D:/Qt/Qt5.12.5/Tools/QtCreator/share/qtcreator # @@ -20,13 +24,15 @@ FORMS += \ HEADERS += \ mainwindow.h \ playimage.h \ - screenrect.h + screenrect.h \ + windowrect.h SOURCES += \ main.cpp \ mainwindow.cpp \ playimage.cpp \ - screenrect.cpp + screenrect.cpp \ + windowrect.cpp # Default rules for deployment. diff --git a/FunctionalModule/SnippingTool/mainwindow.cpp b/FunctionalModule/SnippingTool/mainwindow.cpp index 5693f01..a0e5a07 100644 --- a/FunctionalModule/SnippingTool/mainwindow.cpp +++ b/FunctionalModule/SnippingTool/mainwindow.cpp @@ -9,12 +9,15 @@ #include #include #include +#include +#include MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); + this->setWindowTitle(QString("Qt-截图工具 - V%1").arg(APP_VERSION)); // 设置工具栏 QAction* acNew = new QAction("新建截图"); @@ -39,7 +42,7 @@ MainWindow::MainWindow(QWidget *parent) connect(acSave, &QAction::triggered, this, &MainWindow::on_saveImage); connect(acClear, &QAction::triggered, this, &MainWindow::on_clearImage); connect(&m_screenRect, &ScreenRect::selectRect, this, &MainWindow::on_selectRect); - + connect(&m_windowRect, &WindowRect::selectRect, this, &MainWindow::on_selectRect); } @@ -72,7 +75,8 @@ void MainWindow::on_newGrab(bool checked) } else if(strModel == "窗口") { - + this->hide(); + m_windowRect.show(); } else { @@ -85,15 +89,23 @@ void MainWindow::on_newGrab(bool checked) */ void MainWindow::grabPixmap(QRect rect) { -// this->hide(); // 截图之前将当前窗口隐藏,避免截取的图像中包含当前窗口,这种方法很慢,需要延时等待几百毫秒,否则还是会有当前窗口 - +#if defined(Q_OS_WIN) setWindowOpacity(0); // 最好的方法是将当前窗口设置成完全透明 QDesktopWidget *desk = QApplication::desktop(); // 获取桌面根窗口 QScreen * screen = QGuiApplication::primaryScreen(); // 获取默认屏幕 m_pixmap = screen->grabWindow(desk->winId(), rect.x(), rect.y(), rect.width(), rect.height()); // 抓取屏幕图像 ui->centralwidget->updatePixmap(m_pixmap); // 显示捕获的图像 -// this->show(); // 截图完成后显示窗口 setWindowOpacity(1); +#elif defined(Q_OS_LINUX) + // linux下setWindowOpacity设置透明后截图还可以看到一个透明的边框,效果不是很好,所以使用hide + this->hide(); // 截图之前将当前窗口隐藏,避免截取的图像中包含当前窗口,这种方法很慢,需要延时等待几百毫秒,否则还是会有当前窗口 + QThread::msleep(300); + QDesktopWidget *desk = QApplication::desktop(); // 获取桌面根窗口 + QScreen * screen = QGuiApplication::primaryScreen(); // 获取默认屏幕 + m_pixmap = screen->grabWindow(desk->winId(), rect.x(), rect.y(), rect.width(), rect.height()); // 抓取屏幕图像 + ui->centralwidget->updatePixmap(m_pixmap); // 显示捕获的图像 + this->show(); // 截图完成后显示窗口 +#endif } /** @@ -105,7 +117,9 @@ void MainWindow::on_saveImage(bool checked) Q_UNUSED(checked) if(m_pixmap.isNull()) return; - QString strName = QFileDialog::getSaveFileName(this, "保存到", "./", "便携式网络图形(*.png);;JPEG文件(*.jpg)"); + // linux下getSaveFileName不会返回默认文件后缀,所以需要在文件名中添加后缀,否则QImage::save无法通过后缀推测出文件类型,就会保存失败 + QString name = QString("%1.png").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh-mm-ss")); + QString strName = QFileDialog::getSaveFileName(this, "保存到", name, "便携式网络图形(*.png);;JPEG文件(*.jpg)"); if(strName.isEmpty()) return; QImage image = m_pixmap.toImage(); @@ -115,7 +129,7 @@ void MainWindow::on_saveImage(bool checked) } else { - qDebug() << "保存失败!"; + QMessageBox::warning(this, "注意!", "文件保存失败,请检查有没有输入文件后缀名。"); } } diff --git a/FunctionalModule/SnippingTool/mainwindow.h b/FunctionalModule/SnippingTool/mainwindow.h index a97e515..ab31ba4 100644 --- a/FunctionalModule/SnippingTool/mainwindow.h +++ b/FunctionalModule/SnippingTool/mainwindow.h @@ -4,6 +4,7 @@ #include #include #include "screenrect.h" +#include "windowrect.h" QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } @@ -30,5 +31,6 @@ private: QToolButton* m_butModel = nullptr; QPixmap m_pixmap; // 保存截取的图像 ScreenRect m_screenRect; + WindowRect m_windowRect; }; #endif // MAINWINDOW_H diff --git a/FunctionalModule/SnippingTool/windowrect.cpp b/FunctionalModule/SnippingTool/windowrect.cpp new file mode 100644 index 0000000..556ba77 --- /dev/null +++ b/FunctionalModule/SnippingTool/windowrect.cpp @@ -0,0 +1,216 @@ +#include "windowrect.h" +#include +#include +#include +#include + +#if defined(Q_OS_WIN) +#include +#include +#elif defined(Q_OS_LINUX) +#include +#include +#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: // 鼠标左键按下 + { + emit MouseEvent::getInstance()->mouseSignal(new QMouseEvent(QEvent::MouseButtonPress, QCursor::pos(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier)); + break; + } + default: + break; + } + return CallNextHookEx(nullptr, nCode, wParam, lParam); // 注意这一行一定不能少,否则会出大问题 +} +#endif + +WindowRect::WindowRect(QWidget *parent) : QWidget(parent) +{ +#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); // 设置顶级窗口,防止遮挡 + + // 在当前窗口上增加一层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(MouseEvent::getInstance(), &MouseEvent::mouseSignal, this, &WindowRect::on_mouseSignal); + // 使用定时器定时获取当前鼠标位置的窗口位置信息 + connect(&m_timer, &QTimer::timeout, this, &WindowRect::on_timeout); + m_timer.start(200); +} + +WindowRect::~WindowRect() +{ +#if defined(Q_OS_WIN) + if(g_hook) + { + bool ret = UnhookWindowsHookEx(g_hook); + if(ret) + { + qDebug() << "卸载鼠标钩子。"; + } + } +#endif +} + +/** + * @brief 通过截图全局鼠标事件将当前窗口大小发生出去 + * @param event + */ +void WindowRect::on_mouseSignal(QEvent *event) +{ + delete event; + this->hide(); + emit this->selectRect(QRect(this->pos(), this->size())); +} + +/** + * @brief Windows使用全局鼠标钩子,显示窗口时挂载鼠标钩子 + * @param event + */ +void WindowRect::showEvent(QShowEvent *event) +{ +#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::showEvent(event); +} + +/** + * @brief 隐藏窗口时卸载鼠标钩子 + * @param event + */ +void WindowRect::hideEvent(QHideEvent *event) +{ +#if defined(Q_OS_WIN) + if(g_hook) + { + bool ret = UnhookWindowsHookEx(g_hook); + if(ret) + { + qDebug() << "卸载鼠标钩子。"; + g_hook = nullptr; + } + } +#endif + QWidget::hideEvent(event); +} + +/** + * @brief linux下使用自带的鼠标点击事件就可以 + * @param event + */ +void WindowRect::mousePressEvent(QMouseEvent *event) +{ +#if defined(Q_OS_LINUX) + this->hide(); + emit this->selectRect(QRect(this->pos(), this->size())); +#endif + QWidget::mousePressEvent(event); +} + + +void WindowRect::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() <windowState(); + + // 注意释放资源 + XFree(children); + XCloseDisplay(display); + +#elif defined(Q_OS_MAC) +#endif +} diff --git a/FunctionalModule/SnippingTool/windowrect.h b/FunctionalModule/SnippingTool/windowrect.h new file mode 100644 index 0000000..d2e6b95 --- /dev/null +++ b/FunctionalModule/SnippingTool/windowrect.h @@ -0,0 +1,58 @@ +/****************************************************************************** + * @文件名 windowrect.h + * @功能 选中鼠标当前位置窗口的类 + * + * @开发者 mhf + * @邮箱 1603291350@qq.com + * @时间 2022/11/19 + * @备注 + *****************************************************************************/ +#ifndef WINDOWRECT_H +#define WINDOWRECT_H + +#include +#include + +class WindowRect : public QWidget +{ + Q_OBJECT +public: + explicit WindowRect(QWidget *parent = nullptr); + ~WindowRect() override; + +signals: + void selectRect(QRect rect); + +private slots: + void on_mouseSignal(QEvent* event); + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + +protected: + void on_timeout(); +private: + QTimer m_timer; +}; + +/** + * 全局鼠标事件单例信号类 + */ +class MouseEvent : public QObject +{ + Q_OBJECT +public: + static MouseEvent* getInstance() + { + static MouseEvent mouseEvent; + return &mouseEvent; + } + +signals: + void mouseSignal(QEvent* event); + +private: + MouseEvent(){} +}; + +#endif // WINDOWRECT_H -- GitLab