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

前言:QT绘图基础

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

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

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

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

回到实际的部分,我们先使用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();
}

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

没错,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_等数据进行矩形绘制,那么就会看到矩形跟随方向键在上下左右移动。

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

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

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

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

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

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

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

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

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

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

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

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

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

正文:截图思路

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

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

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

区域截取状态

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

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

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

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

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

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

  void paintEvent(QPaintEvent *event) override {
if (!isCapturing) {
return;
}
QPainter painter(this);
painter.setPen(QPen(Qt::red));
int w = abs(currX - startX);
int h = abs(currY - startY);
painter.drawRect(startX, startY, w, h);
}

也就是说,按照数据驱动渲染的模型,我们完成了由数据到渲染的部分:

接下来,我们完全只需要关注如何修改数据即可。在本例中,我们的操作行为是按下鼠标开始截取区域,移动过程中界面绘制开始点和当前鼠标构成的矩形,松开鼠标完成区域截取。很明显,我们会利用到鼠标事件。在QT中提供了三个鼠标事件供我们使用:

  1. mousePresssEvent,鼠标按下事件;
  2. mouseReleaseEvent,鼠标松开事件;
  3. mouseMoveEvent,鼠标移动事件。

当我们按下鼠标的时候,就进入了“捕获状态”(isCapturing置为true),并且记录鼠标此时按下的位置(startXstartY);在鼠标移动过程中,不断的更新当前鼠标位置(设置currXcurrY);松开鼠标时就退出“捕获状态”(isCapturing置为false)。代码如下:

  void mousePressEvent(QMouseEvent *event) override {
isCapturing = true;
startX = event->pos().x();
startY = event->pos().y();
this->update();
}
void mouseReleaseEvent(QMouseEvent *event) override {
isCapturing = false;
this->update();
}
void mouseMoveEvent(QMouseEvent *event) override {
auto pos = event->pos();
currX = pos.x();
currY = pos.y();
this->update();
}

注意事项1:这里每一个操作都要调用update告诉QT需要触发绘图事件,否则你会发现界面上没有任何的动静。另外,怎么知道什么时候应该调用update方法呢?很简单,只要在某处的代码修改了paintEvent中所依赖的数据,就应该在之后调用update

注意事项2:在QT中,mouseMoveEvent并不是随时都在触发,该事件默认只有在鼠标按下以后的移动过程才会触发,QT这样设计考虑的点是因为鼠标的移动是很频繁的,随时触发会降低性能。如果你在某些场景下就是需要随时出发移动事件,需要在控件的构造函数中调用"setMouseTracking(true);"(可以看代码清单图中11行)。

区域捕获到这里就结束了吗?非也。让我们来演示上面代码的问题:

很明显可以看到,当我们将鼠标向右下拖动的时候,矩形很正常的在动态显示,而向左上角拖动的时候,就出现了问题。原因在于,QT的drawRect等API绘制矩形的时候,位置参数总是矩形的左上角位置,而我们总是将鼠标按下的位置作为左上角位置。然而,鼠标按下的位置就应该是矩形的左上角吗?不总是。当我们拖动鼠标向右下角移动的时候,左上角的start位置确实是可以作为矩形的xy坐标。但一旦我们将鼠标移动到左上角,位于起始位置的左边和上边的时候,就应该用当前鼠标的位置作为矩形的左上角了:

于是,我们需要适当修改以下paintEvent中的代码:

  void paintEvent(QPaintEvent *event) override {
if (!isCapturing) {
return;
}
QPainter painter(this);
painter.setPen(QPen(Qt::red));
int w = abs(currX - startX);
int h = abs(currY - startY);
+ int left = startX < currX ? startX : currX;
+ int top = startY < currY ? startY : currY;
- painter.drawRect(startX, startY, w, h);
+ painter.drawRect(left, top, w, h);
}

就能看到合适的效果了:

捕获完成状态与整体流转

一般截图工具都会在我们松开鼠标的时候,将被截取的区域固定下来,然后我们可以在上面写写画画(譬如添加额外的标记、文字等)。为了达到这个目的,我们首先要考虑如何将一个区域“固定”下来。在前面,我们引入了一个状态:“是否正在捕获中”(使用isCapturing作为标记)。在这里,为了描述“区域截取完成之后”的情形,我们需要引入一个新的状态:截取完成。于是,在整个截图操作的过程中,我们的状态流转如下:

为了后续代码更好的设计,我们使用枚举来表达状态:

enum Status {
Explore = 0,
Capturing,
Captured
};

这里的Status::CapturingStatus::Captured不必多说,要单独解释一下Explore单词的含义。实际上,Explore就是指上面的“默认”,只是在笔者看来,当我们还没有进行截图的时候,鼠标就是在整个窗口上移动“探索”,所以笔者将这个状态取名为Explore

然后,我们需要对现有的代码进行适当的修改。首先是成员变量,由于我们引入了枚举来表达截图的状态,所以原先isCapturing字段就可以舍弃,取而代之的是使用枚举并默认为Status::Explore。同时,我们还需要引入一个矩形数据变量,来存储当我们松开鼠标的时候,截取到的区域的矩形信息。于是变动如下:

private:
int startX = 0, startY = 0;
int currX = 0, currY = 0;
- bool isCapturing;
+ QRect capturedRect; // 存储截取的区域信息,这里使用QT的QRect类
+ Status status = Explore; // 替换原有的bool,并默认为Explore状态

对于数据的定义发生了变化,我们优先考虑渲染部分的变化,也就是paintEvent需要做出适配。正对不同的状态,paintEvent会绘制不同的效果:

  1. Explore态,我们认为界面上什么操作也没有,所以什么都不需要做;
  2. Capturing态,其实就是我们上面isCapturingtrue的处理;
  3. Captured态,截取完成后,我们把截取到的区域用蓝色矩形框住,而矩形数据就是上面新增的成员变量capturedRect

于是,整个代码如下:

void paintEvent(QPaintEvent *event) override {
if (status == Explore) {
return;
}
if (status == Capturing) {
QPainter painter(this);
painter.setPen(QPen(Qt::red));
int w = abs(currX - startX);
int h = abs(currY - startY);
int left = startX < currX ? startX : currX;
int top = startY < currY ? startY : currY;
painter.drawRect(left, top, w, h);
return;
}
if (status == Captured) {
QPainter painter(this);
painter.setPen(QPen(Qt::blue));
painter.drawRect(capturedRect);
return;
}
}

同样的,考虑完了数据以及如何绘制以后,我们需要回到模型的“数据操作”部分,考虑这些数据是如何变化的。按照上面的"默认" -> "截图中" -> "截图后"状态流转图,我们就可以很轻易写出数据修改的代码。

首先是鼠标按下事件。当鼠标按下的时候,如果我们处于Explore,那么就进入Capturing并记录鼠标起始位置;如果处于Captured,那么就什么也不干(理论上是不会有Capturing情况下的鼠标按下事件的),代码如下:

void mousePressEvent(QMouseEvent *event) override {
switch (status) {
case Explore: {
status = Capturing; // 进入Capturing
startX = event->pos().x();
startY = event->pos().y();
break;
}
default:break; // 其余状态都不关心
}
this->update();
}

接着是鼠标松开事件。当鼠标松开的时候,如果是Explore(理论上是不会出现的)或Captured,就什么也不做;如果是Capturing,则进行Captured状态,同时要存储下此时截取的区域,代码如下:

void mouseReleaseEvent(QMouseEvent *event) override {
switch (status) {
case Capturing: {
// 进入Captured态
status = Captured;
// 保存区域
int w = abs(currX - startX);
int h = abs(currY - startY);
int left = startX < currX ? startX : currX;
int top = startY < currY ? startY : currY;
capturedRect = QRect(left, top, w, h);
break;
}
default: break;
}
this->update();
}

然后是鼠标移动过程的状态处理。如果是Explore或是Captured,那么什么也不做;如果是Capturing,那么不断更新当前鼠标位置,代码如下:

void mouseMoveEvent(QMouseEvent *event) override {
switch (status) {
case Capturing: {
auto pos = event->pos();
currX = pos.x();
currY = pos.y();
break;
}
default:break;
}
this->update();
}

此时,我们还差一个将状态从Caputred切回到Explore的处理,我们重写keyPressEvent事件的,如果在Captured状态按下了ECS,就进入Explore态:

void keyPressEvent(QKeyEvent *event) override {
if (event->key() == Qt::Key_Escape) {
status = Explore;
}
this->update();
}

在所有代码准备好以后,让我们启用应用看一下效果:

细心的读者如果实践到此处,会发现一个小问题:每一次按下ESC键以后,下一次进入Capturing状态,在鼠标拖动开始的一瞬间,会有一个矩形框闪现,原因是currXcurrY还是上一次的数据,没有即时清理。解决办法也比较简单,就是在按下的一瞬间,同时更新startcurr的坐标数据为同一位置即可:

void mousePressEvent(QMouseEvent *event) override {
switch (status) {
case Explore: {
status = Capturing; // 进入Capturing
startX = event->pos().x();
startY = event->pos().y();
+ currX = startX; // 同时更新start和curr
+ currY = startY;
break;
}
default:break; // 其余状态都不关心
}
this->update();
}

完成图像截取

终于,我们还剩最后一步了,就是截取这个区域的图像。在之前的介绍中,我们一直在一个空白的窗体上进行绘图。在本节,我们将通过QT的API,来获取当前鼠标所在的屏幕图像,并把图像作为这个窗体的背景图。然后,我们照旧在上面进行区域的截取,来达到所谓的屏幕截图的效果。

首先,我们需要做一些准备工作:

准备工作以下几步:

  1. DemoWidget类中定义一个QImage的指针类成员变量;
  2. 修改构造函数,让外部传入这个QImage实例指针并进行存储;
  3. 调用如下QT提供的相关API来获取屏幕图像:
// 获取鼠标所在屏幕
QScreen *screen = QApplication::screenAt(QCursor().pos());
// 获取屏幕的图像数据
QImage screenImg = screen->grabWindow(0).toImage();
  1. 我们将screenImg的地址作为指针变量作为DemoWidget的构造函数入参传入。

图像的获取与存储完成以后,我们将会在paintEvent中,优先绘制屏幕图像,然后才根据状态来绘制对应的矩形:

于是,界面运行以后,我们就能看屏幕截图填充在窗口里面的效果:

接下来,我们增加一种操作:当处于屏幕截取完成的状态(Captured)的时候,只要按下回车键,就能将截取的屏幕保存到粘贴板中,并回到Explore状态。很自然的,我们需要在keyPressEvent新增关于该操作的代码:

void keyPressEvent(QKeyEvent *event) override {
+ if (event->key() == Qt::Key_Return && status == Captured) {
+ // 1. 获取捕获的图像区域
+ // 2. 从保存的屏幕图像中获取指定区域的图像数据
+ // 3. 将图像数据写入到操作系统粘贴板
+ // 4. 回到Explore
+ return;
+ }
if (event->key() == Qt::Key_Escape) {
status = Explore;
}
this->update();
}

注意,QT中回车键的枚举值是Key_Return,不是Key_Enter。

对于步骤1,我们在前文已经使用capturedRect类成员变量保存了当区域截取完成以后的区域数据;

对于步骤2,QImage有一个名为copy的方法:

[[nodiscard]] QImage copy(int x, int y, int w, int h) const;

它可以从已有的图像中复制指定区域的图像,得到一个新的图像数据;

对于步骤3,我们可以使用QT提供的QClipboard类来操作系统粘贴板。于是,你可以这样调用来将图像数据保存到粘贴板中:

QClipboard *clipboard = QGuiApplication::clipboard();
clipboard->setImage(/* QImage对象 */);

对于步骤4就比较简单了,切换status的状态为Explore即可。

按照上面的过程描述,我们编写如下的代码:

void keyPressEvent(QKeyEvent *event) override {
if (event->key() == Qt::Key_Enter && status == Captured) {
// 1. 获取捕获的图像区域
auto imgRect = this->capturedRect;
// 2. 从保存的屏幕图像中获取指定区域的图像数据
auto copiedImg = this->screenImg->copy(imgRect);
// 3. 将图像数据写入到操作系统粘贴板
QClipboard *clipboard = QGuiApplication::clipboard();
clipboard->setImage(copiedImg);
// 4. 回到Explore
status = Explore;
return;
}
// 其余代码 ... ...
}

当我们兴致勃勃的运行应用并进行截图操作的时候,会发现在粘贴板中的图像,和我们截取的区域不太一致!

注意,我们截取了右下角有紫蓝色的区域,但是实际获取的图像却不是。这个问题的核心原因是,我们截取的capturedRect是这个窗体界面上的区域,但并不是图像真正的区域capturedRect需要进行比例转换,才能得到实际在图片上的区域。

也就是说,我们需要将capturedRect转化为实际imgRect:

void keyPressEvent(QKeyEvent *event) override {
if (event->key() == Qt::Key_Return && status == Captured) {
// 1. 获取捕获的图像区域
auto picRealSize = screenImg->size();
auto winSize = this->size();
// 比例计算
int realRectX = capturedRect.x() * picRealSize.width() / winSize.width();
int realRectY = capturedRect.y() * picRealSize.height() / winSize.height();
int realRectW =
capturedRect.width() * picRealSize.width() / winSize.width();
int realRectH =
capturedRect.height() * picRealSize.height() / winSize.height();
// 得到实际Rect
QRect imgRect(realRectX, realRectY, realRectW, realRectH);
// 2. 从保存的屏幕图像中获取指定区域的图像数据
auto copiedImg = this->screenImg->copy(imgRect);
// 3. 将图像数据写入到操作系统粘贴板
QClipboard *clipboard = QGuiApplication::clipboard();
clipboard->setImage(copiedImg);
// 4. 回到Explore
status = Explore;
return;
}
if (event->key() == Qt::Key_Escape) {
status = Explore;
}
this->update();
}

按照比例换算以后的代码如上,此时我们再看效果,会发现没有问题了:

最后

这篇文章算不上是比较深入的讲解截图工具的实现,只是通过demo来大体上讲解了截图的机制,让读者有一个入门的认识,像是截图区域确定以后我们还可以在上面添加方框、圆形、文字等操作都没有在这篇文章中体现。这篇文章只是一个入门,读者可以在掌握了基本的开发模式以后,实现更有意思的功能。

另外,笔者自己编写的截图软件capi(仓库地址:w4ngzhen/capi)已经有了基本的雏形,后续还会持续的往里面增加功能的,这里厚着脸皮希望有小伙伴能给个start。值得提到的是,笔者的截图软件capi目前是基于QT编写的,但是笔者正在做的是将截图的模块和QT的模块进行完全的解耦(其实已经差不多了),使用C++17的标准实现了截图功能核心模块的概念抽象,其目的在于笔者准备将QT换成另一个跨平台GUI框架wxWidgets来实现,为了实现这个目的,截图模块与具体的GUI框架解耦是十分必要的。

回到本文相关的内容,整篇文章的的demo只有一个cpp文件(QT的环境配置请自行解决啦),我直接放到Github gist:

simple screen capture demo based Qt (github.com)/

浅谈基于QT的截图工具的设计与实现的更多相关文章

  1. 浅谈基于Prism的软件系统的架构设计

    很早就想写这么一篇文章来对近几年使用Prism框架来设计软件来做一次深入的分析了,但直到最近才开始整理,说到软件系统的设计这里面有太多的学问,只有经过大量的探索才能够设计出好的软件产品,就本人的理解, ...

  2. 浅谈关于QT中Webkit内核浏览器

    关于QT中Webkit内核浏览器是本文要介绍的内容,主要是来学习QT中webkit中浏览器的使用.提起WebKit,大家自然而然地想到浏览器.作为浏览器内部的主要构件,WebKit的主要工作是渲染.给 ...

  3. 【大型软件开发】浅谈大型Qt软件开发(二)面向未来开发——来自未来的技术:COM组件。我如何做到让我们的教学模块像插件一样即插即用,以及为什么这么做。

    前言 最近我们项目部的核心产品正在进行重构,然后又是年底了,除了开发工作之外项目并不紧急,加上加班时间混不够了....所以就忙里偷闲把整个项目的开发思路聊一下,以供参考. 鉴于接下来的一年我要进行这个 ...

  4. 【大型软件开发】浅谈大型Qt软件开发(三)QtActive Server如何通过COM口传递自定义结构体?如何通过一个COM口来获得所有COM接口?

    前言 最近我们项目部的核心产品正在进行重构,然后又是年底了,除了开发工作之外项目并不紧急,加上加班时间混不够了....所以就忙里偷闲把整个项目的开发思路聊一下,以供参考. 鉴于接下来的一年我要进行这个 ...

  5. 【大型软件开发】浅谈大型Qt软件开发(一)开发前的准备——在着手开发之前,我们要做些什么?

    前言 最近我们项目部的核心产品正在进行重构,然后又是年底了,除了开发工作之外项目并不紧急,加上加班时间混不够了....所以就忙里偷闲把整个项目的开发思路聊一下,以供参考. 鉴于接下来的一年我要操刀这个 ...

  6. 浅谈基于Linux的Redis环境搭建

    本篇文章主要讲解基于Linux环境的Redis服务搭建,Redis服务配置.客户端访问和防火强配置等技术,适合具有一定Linux基础和Redis基础的读者阅读. 一  Redis服务搭建 1.在根路径 ...

  7. 【译】浅谈微软OneNote的自动化测试工具

    当我们向人们介绍OneNote的自动化时,有一个问题被相当频繁地提到,担忧我们的自动化框架中UI层面测试偏少. 我不喜欢基于UI的自动化.我知道在市场上有许多的自动化系统都是基于UI的自动化(点击按钮 ...

  8. 浅谈基于WOPI协议实现跨浏览器的Office在线编辑解决方案

    如今,基于Web版的Office 在线预览与编辑功能已成为一种趋势,而关于该技术的实现却成为了国内大部份公司的技术挑战,挑战主要存在于两方面: 其一:目前国内乃至微软本身,还没有相对较为完善的解决方案 ...

  9. 软件安全测试新武器 ——浅谈基于Dynamic Taint Propagation的测试技术

    软件安全测试是保证软件能够安全使用的最主要的手段,如何进行高效的安全测试成为业界关注的话题.多年的安全测试经验告诉我们,做好软件安全测试的必要条件是:一是充分了解软件安全漏洞,二是拥有高效的软件安全测 ...

  10. 浅谈前端常用脚手架cli工具及案例

    前端常用脚手架工具 前端有很多特定的脚手架工具大多都是为了特定的项目类型服务的,比如react项目中的reate-react-app,vue项目中的vue-cli,angular 项目中的angula ...

随机推荐

  1. 最详细的Git命令大全

    Git常用命令及方法大全 下面是我整理的常用 Git 命令清单.几个专用名词的译名如下. Workspace:工作区 Index / Stage:暂存区 Repository:仓库区(或本地仓库) R ...

  2. CentOS 8 已是绝版?还有后续么?

    文章由 Linux爱好者( ID: LinuxHub)整理自开源中国 + 红帽官方.本文章经原作者同意后授权转载. 2020年12月8日,CentOS 项目宣布,CentOS 8 将于 2021 年底 ...

  3. R画韦恩图之总结

    本文分享自微信公众号 - 生信科技爱好者(bioitee).如有侵权,请联系 support@oschina.cn 删除.本文参与"OSC源创计划",欢迎正在阅读的你也加入,一起分 ...

  4. C# 客户端程序 Visual Studio 远程调试方法

    传统桌面客户端的远程调试相比UWP,ASP等项目来说,配置比较麻烦,因为它是非部署的应用程序,原理是复制编译的文件到远程计算机,通过网络来连接和VS的通信,本文主要讲述WPF,WinForm应用程序的 ...

  5. 确保使用正确的CSI提交HW问题

    最近有用户一体机有问题,需要技术支持,首先找到我这边,其实就是一个简单的坏盘类问题,换盘即可. 在保期间,要求客户提交一个SR给后台,但是客户提交后,就一直被要求提供HW的CSI号: xxx: Can ...

  6. MySQL存储之为什么要使用B+树做为储存结构?

    导言: 在使用MySQL数据库的时候,我们知道了它有两种物理存储结构,hash存储和B+树存储,由于hash存储使用的少,而B+树存储使用的范围就多些,如 InnoDB和MYISAM引擎都是使用的B+ ...

  7. 力扣 662 https://leetcode.cn/problems/maximum-width-of-binary-tree/

    需要了解树的顺序存储 如果是普通的二叉树 ,底层是用链表去连接的 如果是满二叉树,底层用的是数组去放的,而数组放的时候 会有索引对应 当前父节点是索引i,下一个左右节点就是2i,2i+1 利用满二叉树 ...

  8. 使用C#编写.NET分析器(三)

    译者注 这是在Datadog公司任职的Kevin Gosse大佬使用C#编写.NET分析器的系列文章之一,在国内只有很少很少的人了解和研究.NET分析器,它常被用于APM(应用性能诊断).IDE.诊断 ...

  9. 即构携手智能对讲机品牌Runbo,打造可视化对讲通信系统

    现代通信技术的发展,让信息的传递变得前所未有的便捷.作为双向移动通信工具,对讲机不需要网络支持就可以进行通话,且没有话费产生,尤其适合酒店.物业等使用区域固定.通话频繁的场景. 随着技术的不断迭代,对 ...

  10. 图像增强—自适应直方图均衡化(AHE)-限制对比度自适应直方图均衡(CLAHE)

    一.自适应直方图均衡化(Adaptive histgram equalization/AHE) 1.简述 自适应直方图均衡化(AHE)用来提升图像的对比度的一种计算机图像处理技术.和普通的直方图均衡算 ...