本文介绍了一个基于Qt框架的屏幕GIF录制工具的实现。该工具包含XYGifCreator类负责GIF创建逻辑,使用Gif.h库进行GIF编码;XYGifFrame类提供GUI界面,支持设置录制区域大小、帧率以及保存位置。工具采用多线程处理GIF编码,支持Windows和Linux平台,能够捕获屏幕内容并包含鼠标指针。实现功能包括:区域选择调整、GIF录制控制、系统托盘图标操作等。核心代码展示了如何通过Qt跨平台API获取屏幕截图,并针对不同操作系统处理鼠标指针绘制,最后将图像帧编码为GIF动画。
#include "xygifcreator.h"
#include <QImage>
#include <QThread>#include "gif.h"class XYGif : public QObject
{Q_OBJECT
public:explicit XYGif(){moveToThread(&mWorkThread);}~XYGif(){if (mWorkThread.isRunning()){mWorkThread.quit();mWorkThread.wait();}}public slots:void begin(const QString &file, int width, int height, int delay){GifBegin(&mGifWriter, file.toUtf8().data(), static_cast<quint32>(width), static_cast<quint32>(height), static_cast<quint32>(delay));}void frame(const QImage &img, int width, int height, int delay){GifWriteFrame(&mGifWriter, img.bits(),static_cast<quint32>(qMin(width, img.width())),static_cast<quint32>(qMin(height, img.height())),static_cast<quint32>(100.0 / delay));}void end(){GifEnd(&mGifWriter);mWorkThread.quit();}private:GifWriter mGifWriter;QThread mWorkThread;friend class XYGifCreator;
};XYGifCreator::XYGifCreator(QObject *parent): QObject(parent)
{mGif = new XYGif;connect(&mGif->mWorkThread, &QThread::finished, this, &XYGifCreator::finished);
}XYGifCreator::~XYGifCreator()
{delete mGif;
}void XYGifCreator::startThread()
{mGif->mWorkThread.start();
}bool XYGifCreator::isRunning()
{return mGif->mWorkThread.isRunning();
}void XYGifCreator::begin(const QString &file, int width, int height, int delay, Qt::ConnectionType type)
{mWidth = width;mHeight = height;QMetaObject::invokeMethod(mGif, "begin", type,QGenericReturnArgument(),Q_ARG(QString, file),Q_ARG(int, width),Q_ARG(int, height),Q_ARG(int, delay));
}void XYGifCreator::frame(const QImage &img, int delay, Qt::ConnectionType type)
{// gif.h 文件有描述目前只能是RGBA8888图片格式,并且alpha没有被使用QImage img32 = img.convertToFormat(QImage::Format_RGBA8888);QMetaObject::invokeMethod(mGif, "frame", type,QGenericReturnArgument(),Q_ARG(QImage, img32),Q_ARG(int, mWidth),Q_ARG(int, mHeight),Q_ARG(int, delay));
}void XYGifCreator::end(Qt::ConnectionType type)
{QMetaObject::invokeMethod(mGif, "end", type);
}#include "xygifcreator.moc"
#include "xygifframe.h"
#include "xypackimage.h"
#include "ui_xygifframe.h"
#include <QPainter>
#include <QFileDialog>
#include <QApplication>
#include <QScreen>
#include <QResizeEvent>
#include <QDateTime>
#include <QMessageBox>
#include <QDebug>
#include <QMenu>#ifdef Q_OS_WIN32
#include <windows.h>static QImage getScreenImage(int x, int y, int width, int height)
{HDC hScrDC = ::GetDC(nullptr);HDC hMemDC = nullptr;BYTE *lpBitmapBits = nullptr;int nWidth = width;int nHeight = height;hMemDC = ::CreateCompatibleDC(hScrDC);BITMAPINFO bi;ZeroMemory(&bi, sizeof(BITMAPINFO));bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);bi.bmiHeader.biWidth = nWidth;bi.bmiHeader.biHeight = nHeight;bi.bmiHeader.biPlanes = 1;bi.bmiHeader.biBitCount = 24;CURSORINFO hCur ;ZeroMemory(&hCur,sizeof(hCur));hCur.cbSize = sizeof(CURSORINFO);GetCursorInfo(&hCur);ICONINFO IconInfo = {};if(GetIconInfo(hCur.hCursor, &IconInfo)){hCur.ptScreenPos.x -= IconInfo.xHotspot;hCur.ptScreenPos.y -= IconInfo.yHotspot;if(nullptr != IconInfo.hbmMask)DeleteObject(IconInfo.hbmMask);if(nullptr != IconInfo.hbmColor)DeleteObject(IconInfo.hbmColor);}HBITMAP bitmap = ::CreateDIBSection(hMemDC, &bi, DIB_RGB_COLORS, (LPVOID*)&lpBitmapBits, nullptr, 0);HGDIOBJ oldbmp = ::SelectObject(hMemDC, bitmap);::BitBlt(hMemDC, 0, 0, nWidth, nHeight, hScrDC, x, y, SRCCOPY);DrawIconEx(hMemDC, hCur.ptScreenPos.x - x, hCur.ptScreenPos.y - y, hCur.hCursor, 0, 0, 0, nullptr, DI_NORMAL | DI_COMPAT);BITMAPFILEHEADER bh;ZeroMemory(&bh, sizeof(BITMAPFILEHEADER));bh.bfType = 0x4d42; //bitmapbh.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);bh.bfSize = bh.bfOffBits + ((nWidth*nHeight)*3);int size = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + 3 * nWidth * nHeight;uchar *bmp = new uchar[size];uint offset = 0;memcpy(bmp, (char *)&bh, sizeof(BITMAPFILEHEADER));offset = sizeof(BITMAPFILEHEADER);memcpy(bmp + offset, (char *)&(bi.bmiHeader), sizeof(BITMAPINFOHEADER));offset += sizeof(BITMAPINFOHEADER);memcpy(bmp + offset, (char *)lpBitmapBits, 3 * nWidth * nHeight);::SelectObject(hMemDC, oldbmp);::DeleteObject(bitmap);::DeleteObject(hMemDC);::ReleaseDC(nullptr, hScrDC);QImage image = QImage::fromData(bmp, size);delete[] bmp;return image;
}
#else // linux x11
#include <X11/Xlib.h>
#include <X11/extensions/Xfixes.h>
static void X11drawCursor(Display *display, QImage &image, int recording_area_x, int recording_area_y)
{// get the cursorXFixesCursorImage *xcim = XFixesGetCursorImage(display);if(xcim == NULL)return;// calculate the position of the cursorint x = xcim->x - xcim->xhot - recording_area_x;int y = xcim->y - xcim->yhot - recording_area_y;// calculate the part of the cursor that's visibleint cursor_left = std::max(0, -x), cursor_right = std::min((int) xcim->width, image.width() - x);int cursor_top = std::max(0, -y), cursor_bottom = std::min((int) xcim->height, image.height() - y);unsigned int pixel_bytes, r_offset, g_offset, b_offset; // ARGBpixel_bytes = 4;r_offset = 2; g_offset = 1; b_offset = 0;// draw the cursor// XFixesCursorImage uses 'long' instead of 'int' to store the cursor images, which is a bit weird since// 'long' is 64-bit on 64-bit systems and only 32 bits are actually used. The image uses premultiplied alpha.for(int j = cursor_top; j < cursor_bottom; ++j) {unsigned long *cursor_row = xcim->pixels + xcim->width * j;uint8_t *image_row = (uint8_t*) image.bits() + image.bytesPerLine() * (y + j);for(int i = cursor_left; i < cursor_right; ++i) {unsigned long cursor_pixel = cursor_row[i];uint8_t *image_pixel = image_row + pixel_bytes * (x + i);int cursor_a = (uint8_t) (cursor_pixel >> 24);int cursor_r = (uint8_t) (cursor_pixel >> 16);int cursor_g = (uint8_t) (cursor_pixel >> 8);int cursor_b = (uint8_t) (cursor_pixel >> 0);if(cursor_a == 255) {image_pixel[r_offset] = cursor_r;image_pixel[g_offset] = cursor_g;image_pixel[b_offset] = cursor_b;} else {image_pixel[r_offset] = (image_pixel[r_offset] * (255 - cursor_a) + 127) / 255 + cursor_r;image_pixel[g_offset] = (image_pixel[g_offset] * (255 - cursor_a) + 127) / 255 + cursor_g;image_pixel[b_offset] = (image_pixel[b_offset] * (255 - cursor_a) + 127) / 255 + cursor_b;}}}// free the cursorXFree(xcim);
}
#endifXYGifFrame::XYGifFrame(QWidget *parent): XYMovableWidget(parent),mStartResize(false),ui(new Ui::XYGifFrame)
{ui->setupUi(this);mGifCreator = new XYGifCreator(this);mTimer.setSingleShot(false);setWindowFlags( Qt::WindowStaysOnTopHint);ui->width->installEventFilter(this);ui->height->installEventFilter(this);connect(ui->width, SIGNAL(editingFinished()), this, SLOT(doResize()));connect(ui->height, SIGNAL(editingFinished()), this, SLOT(doResize()));connect(ui->gif, SIGNAL(clicked()), this, SLOT(active()));connect(ui->img, SIGNAL(clicked()), this, SLOT(packImages()));connect(&mTimer, SIGNAL(timeout()), this, SLOT(frame()));connect(ui->save, SIGNAL(clicked()), this, SLOT(setGifFile()));connect(mGifCreator, &XYGifCreator::finished, this, [this](){this->ui->tips->setText(QStringLiteral("保存Gif完成!"));});ui->content->adjustSize();setMouseTracking(true);setMinimumSize(162, 150);ui->gif->setFocus();m_tray = new QSystemTrayIcon(this);//实例化QPixmap m_logo(":/gif.ico");m_tray->setIcon(QIcon(m_logo));//设置图标m_tray->show();connect(m_tray,&QSystemTrayIcon::activated,this,&XYGifFrame::TrayIconAction);m_menu = new QMenu(this);m_resetAction = new QAction(this);m_resetAction->setText("show");m_quitAction = new QAction(this);m_resetAction->setIcon(QIcon(m_logo));m_quitAction->setText("quit");m_quitAction->setIcon(QIcon(m_logo));connect(m_quitAction,&QAction::triggered,qApp,&QApplication::quit);connect(m_resetAction,&QAction::triggered,this,&XYGifFrame::restory);
// connect(m_closeDialog,&closeDialog::traySiganal,this,&XYGifFrame::Tray);m_tray->setContextMenu(m_menu);//设置托盘菜单m_menu->addAction(m_resetAction);m_menu->addAction(m_quitAction);}XYGifFrame::~XYGifFrame()
{delete ui;
}void XYGifFrame::doResize()
{QRect rect(pos(), QSize(ui->width->value(), ui->height->value()));rect.adjust(-3, -3, 3, ui->content->height() + 5);resize(rect.size());
}void XYGifFrame::packImages()
{XYPackImage img(this);img.exec();
}void XYGifFrame::setGifFile()
{mGifFile = QFileDialog::getSaveFileName(this, "", QString("xy-%1.gif").arg(QDateTime::currentDateTime().toString("yyyyMMdd-hhmmss")));ui->save->setToolTip(mGifFile);
}void XYGifFrame::active()
{if (!mTimer.isActive()){if (mGifCreator->isRunning()){QMessageBox::warning(this, QStringLiteral("提醒"), QStringLiteral("正在保存Gif,请稍等!"));return;}start();}else{stop();}
}void XYGifFrame::start()
{if (!mGifFile.isEmpty()){mGifCreator->startThread();mGifCreator->begin(mGifFile.toUtf8().data(), ui->width->value(), ui->height->value(), 1);mPixs = 0;ui->gif->setText(QStringLiteral("停止录制"));frame();mTimer.start(static_cast<int>(1000.0 / ui->interval->value()));ui->width->setDisabled(true);ui->height->setDisabled(true);ui->interval->setDisabled(true);}else{QMessageBox::warning(this, QStringLiteral("提醒"), QStringLiteral("请先设置保存gif的位置!"));}
}void XYGifFrame::stop()
{mTimer.stop();ui->gif->setText(QStringLiteral("开始录制"));mGifCreator->end();ui->width->setEnabled(true);ui->height->setEnabled(true);ui->interval->setEnabled(true);this->ui->tips->setText(QStringLiteral("请等待保存Gif..."));
}void XYGifFrame::frame()
{QImage img = getCurScreenImage();if (!img.isNull()){mGifCreator->frame(img, ui->interval->value());mPixs++;ui->tips->setText(QStringLiteral("已保存 %1 张图片").arg(mPixs));}
}bool XYGifFrame::eventFilter(QObject *watched, QEvent *event)
{if (watched == ui->width|| watched == ui->height){if (event->type() == QEvent::Wheel){doResize();}}return XYMovableWidget::eventFilter(watched, event);
}void XYGifFrame::paintEvent(QPaintEvent *)
{QPainter painter(this);painter.fillRect(rect(), Qt::gray);
}void XYGifFrame::resizeEvent(QResizeEvent *)
{QRect rect = this->rect();rect.adjust(3, 3, -3, -(ui->content->height() + 5));mRecordRect = rect;ui->width->setValue(mRecordRect.width());ui->height->setValue(mRecordRect.height());QRegion region(this->rect());setMask(region.xored(mRecordRect));qDebug()<<region.xored(mRecordRect);ui->content->move(width() - ui->content->width() - 3, height() - ui->content->height() - 3);
}void XYGifFrame::mousePressEvent(QMouseEvent *event)
{QRect rect(QPoint(width() - 3, height() - 3), QSize(3, 3));if (rect.contains(event->pos()) && !mTimer.isActive()){mStartResize = true;mStartGeometry = QRect(event->globalPos(), size());}else{XYMovableWidget::mousePressEvent(event);}
}void XYGifFrame::mouseReleaseEvent(QMouseEvent *event)
{mStartResize = false;setCursor(Qt::ArrowCursor);XYMovableWidget::mouseReleaseEvent(event);
}void XYGifFrame::mouseMoveEvent(QMouseEvent *event)
{QRect rect(QPoint(width() - 3, height() - 3), QSize(3, 3));if (mStartResize){QPoint ch = event->globalPos() - mStartGeometry.topLeft();resize(mStartGeometry.size() + QSize(ch.x(), ch.y()));}else if (rect.contains(event->pos()) && !mTimer.isActive()){setCursor(Qt::SizeFDiagCursor);}else{setCursor(Qt::ArrowCursor);XYMovableWidget::mouseMoveEvent(event);}
}void XYGifFrame::wheelEvent(QWheelEvent *event)
{if (event->delta() < 0){ui->content->move(ui->content->x() + 6, ui->content->y());}else{ui->content->move(ui->content->x() - 6, ui->content->y());}XYMovableWidget::wheelEvent(event);
}QImage XYGifFrame::getCurScreenImage()
{QImage img;
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)auto screen = qApp->screenAt(pos());
#elseQScreen *screen = nullptr;foreach (screen, qApp->screens()){if (screen->geometry().contains(pos())){break;}}
#endifif (screen != nullptr){
#ifdef Q_OS_WIN32img = getScreenImage(x() + mRecordRect.x(),y() + mRecordRect.y(),mRecordRect.width(),mRecordRect.height());
#elseimg = screen->grabWindow(0,x() + mRecordRect.x(),y() + mRecordRect.y(),mRecordRect.width(),mRecordRect.height()).toImage();// 需要系统是x11后端auto display = XOpenDisplay(NULL);X11drawCursor(display, img, x() + mRecordRect.x(), y() + mRecordRect.y());XCloseDisplay(display);
#endif}return img;
}void XYGifFrame::on_quit_clicked()
{this->hide();
}