浅谈基于QT的截图工具的设计与实现

本人一直在做属于自己的一款跨平台的截图软件(w4ngzhen/capi(github.com)),在软件编写的过程中有一些心得体会,所以有了本文。其实这篇文章酝酿了很久,现在这款软件有了雏形,也有空梳理并写下这篇循序渐进的介绍截图工具的设计与实现的文章了。

前言:QT绘图基础

在介绍截图工具设计与实现前,让我们先通过介绍QT的绘图基础知识,让读者有一个比较感性的认识。

本文理论上并非是完整的QT框架使用介绍,但是我们总是需要用一款支持绘图的GUI框架来介绍关于截图的知识,于是笔者就拿较为熟悉的QT框架来说明。但只要读者理解到了截图工具的本质,举一反三,其它的GUI框架也能完成截图的目的。

对于绘图来说,我们通常遵循“数据驱动渲染的模型。具体一点,我们会围绕数据展开绘图,图像的绘制总是来源于数据的定义。那么如何实现动态图形呢?只需要通过某些操作改变数据即可。

这样的模型,数据的修改和数据的渲染是解耦的,我们编写处理绘图部分的时候,只需要根据已有的数据进行绘制,可以完全不用关心数据是怎么变化的;而当操作数据的时候,完全可以不用关心渲染部分。基于该模型,可以让我们在开发类似于截图软件的时候,极大降低心智负担。

010-data-opr-render

回到实际的部分,我们先使用QT编写一个窗体widget,然后重写窗体的paintEvent方法:

class DemoWidget: public QWidget {
public:
  void paintEvent(QPaintEvent *event) override {
    QPainter painter(this);
    painter.setPen(QPen(Qt::red));
    painter.drawRect(10, 10, 100, 60);
  }
};

paintEvent函数体代码就三行:

  1. 使用当前窗体指针构造一个QPainter(QPainter painter(this););
  2. 设置画笔的颜色;
  3. 在坐标(10, 10)处绘制一个宽100像素,高60像素的矩形。

然后,我们编写main方法,创建这个DemoWidget类的实例,将它show出来:

int main(int argc, char *argv[]) {
  QApplication a(argc, argv);
  DemoWidget w;
  w.resize(200, 100);
  w.show();
  return QApplication::exec();
}

整体代码和运行效果如下:

020-demo-widget-draw-rect

没错,QT中在一个窗体中进行绘图就是这么简单。接下来让我们更进一步,将矩形数据(x,y,w,h)提升到到类成员变量层级,并让painter绘制矩形的时候读取类成员变量:

class DemoWidget: public QWidget {
public:
  void paintEvent(QPaintEvent *event) override {
    QPainter painter(this);
    painter.setPen(QPen(Qt::red));
-   painter.drawRect(10, 10, 100, 60);
+   painter.drawRect(x_, y_, w_, h_); // 读取类成员变量
  }
+ private:
+  int x_ = 10, y_ = 10, w_ = 100, h_ = 60;
};

然后,我们重写QWidget的onKeyPress事件,代码如下:

void keyPressEvent(QKeyEvent *event) override {
   
  auto key = event->key();
  switch (key) {
   
    case Qt::Key_Up: y_ -= 5;
      break;
    case Qt::Key_Down: y_ += 5;
      break;
    case Qt::Key_Left: x_ -= 5;
      break;
    case Qt::Key_Right: x_ += 5;
      break;
    default:break;
  }
}

这段代码的作用是当我们按下方向键后,就能够修改x_y_变量的值,于是矩形的xy坐标会按照对应方向移动5像素。理论上讲,如果此时触发绘图事件,而我们使用painter又在读取类成员变量x_y_等数据进行矩形绘制,那么就会看到矩形跟随方向键在上下左右移动。

030-data-opr-render-rect-pos

然而,当我们操作时候却发现无论怎么按方向键界面似乎没有任何反应:

040-no-update-keypress

为什么呢?让我们引入qdebug向控制台输出一些信息一探究竟:

050-qdebug

应用运行以后,通过QDebug,我们可以在调试模式下看到控制台的输出内容:

060-console-output-without-update

通过控制台可以看到,一开始触发了几次绘图事件(paintEvent)。之后,当我们按下方向键时,触发了按键事件(keyPressEvent),此时x_y_的值的确已经发生了改变,但是控件上的矩形没有任何的变化。实际上,造成这种问题的根本原因在于我们重写的绘图事件没有触发,于是导致最新的效果并没有绘制到界面上,所以看不出效果。

那么,QT的绘图事件什么时候触发呢?大致会有一下几种情况:

  1. 当控件第一次显示时,系统会自动产生一个绘图事件。比如上面的动图中第一次的paintEvent
  2. 窗体失去焦点,获得焦点等,之后几次paintEvent出发就是因此产生的。
  3. 当窗口控件被其他部件遮挡,然后又显示出来时,会对隐藏的区域产生一个重绘事件。比如最小化再出现。
  4. 重新调整窗口大小时。
  5. repaint()update()函数被调用时。

上面的例子中,在按下方向键以后界面没有效果,如果此时我们最小化它再恢复它,就会看到绘图事件被触发,同时界面也有所改变:

070-minimize-trigger-paint

当然,我们不可能为了触发绘图事件而手动操作窗体。为了达到触发绘图事件的目的,我们一般会调用控件的update方法系列方法或repaint的系列方法,来主动告诉QT需要进行控件的重新绘制,进而让QT触发paintEvent,绘制界面:

080-update-for-paintEvent

再次运行程序,并按下方向键,我们可以清楚的看到paintEvent在每次按下方向键以后都被调用,同时,矩形也表现出移动的效果:

090-update-effective

这里我们调用的是update方法,同时,我们还提到QT还提供一个repaint方法,二者区别在于:repaint一旦调用,QT内部就会立刻调用触发paintEvent,而update只是将触发绘图事件的任务放到事件队列,等统一事件调用。所以,绝对不能在paintEvent中调用repaint,这样会死循环。

此外使用update还有一个优点在于,QT会将多个update的请求通过算法机制尽可能的合并为一个paintEvent,从而提高运行的效率。比如,我们可以在调用update的地方多赋值几次调用:

100-many-update

在实际调用中,只会触发一次paintEvent

110-update-many-trigger-paintEvent

如果换成调用5次repaint就会发现每调用一次就会触发一次paintEvent,读者可以自行测试。

正文:截图思路

在介绍了QT绘图基础以后,我们终于可以开始讨论正题了:截图工具的设计与实现。实际上,截图工具实现起来并不复杂。可以想象一下,我们首先通过某种API获取到桌面屏幕的图片,然后把这个图片放到一个窗体里面,最后再把这个窗体最大化的方式展现在屏幕上。此时就达到了我们截取了屏幕并让整个屏幕“冻结”,等待我们操作的效果。

115-screen-capture

此时窗体全屏幕覆盖,接下来我们就需要在上面进行某个区域的获取。

PS:这个动图使用了跨平台视频剪辑工具Kdenlive制作,并转为gif,有空写一个教程,哈哈。

区域截取状态

一般来说,截图过程就是按下鼠标,然后移动鼠标,此时界面上会显示整个鼠标拖拽产生的一个区域,直到松开鼠标,这个区域就被“截取”下来了:

120-make-capturing

想要实现这样的效果并不复杂,代码如下何解释如下:

130-prepare-and-draw-rect

在上图代码中我分别标注了两个部分:

  1. 捕获指定区域所需要的数据;
  2. 将指定数据转化为图形进行绘制。

首先讲解第一部分:捕获指定区域所需要的数据。这里我使用了三组数据,分别是:鼠标按下的起始位置、鼠标当前的位置、是否处于捕获中状态。不难看出,只需要这三组数据,我们就可以描述这样一个画面:如果没有在捕获状态,那么界面上不会出现矩形;如果处于捕获状态,那么我们使用起始位置和当前位置得到一个矩形:

140-capturing-mouse-pos

paintEvent中的代码实现也正是如此:

  void paintEvent(QPaintEvent *event) override {
   
    if (!isCapturing) {
   
      return
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值