当前位置: 首页 > news >正文

Canvas鼠标滚轮缩放以及画布拖动(图文并茂版)

Canvas鼠标滚轮缩放以及画布拖动

本文会带大家认识Canvas中常用的坐标变换方法 translate 和 scale,并结合这两个方法,实现鼠标滚轮缩放以及画布拖动功能。

Canvas的坐标变换

Canvas 绘图的缩放以及画布拖动主要通过 CanvasRenderingContext2D 提供的 translatescale 两个方法实现的,先来认识下这两个方法。

translate 方法

语法:

translate(x, y)

translate 的用法记住一句话:

translate 方法重新映射画布上的(0, 0)位置。

说白了就是把画布的原点移动到了 translate 方法指定的坐标,之后所有图形的绘制都会以该坐标进行参照。

举个例子:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 600;
canvas.height = 400;ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 50, 50);ctx.translate(50, 50);ctx.fillStyle = 'green';
ctx.fillRect(50, 50, 50, 50);

开始的时候,Canvas 容器原点和绘图原点重合,绘制一个背景色为红色,原点坐标(50, 50),长宽各为 50 的矩形,接着调用 translate 方法将绘图原点沿水平和纵向各偏移50,再绘制一个背景色是绿色,原点坐标(50, 50),长宽各为 50 的矩形,示意图如下,其中灰色的背景为 Canvas 区域。

translate.png

需要注意的是,如果此时继续调用 translate 方法进行偏移操作,后续的偏移会基于原来偏移的基础上进行的。

ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 50, 50);// 第一次坐标系偏移
ctx.translate(50, 50);ctx.fillStyle = 'green';
ctx.fillRect(50, 50, 50, 50);// 第二次坐标系偏移
ctx.translate(50, 50);ctx.fillStyle = 'blue';
ctx.fillRect(50, 50, 50, 50);

第二次translate.png

因此,如果涉及到多次调用 translate 方法进行坐标变换,很容易将坐标系搞混乱,所以,一般在translate 之前会调用 save 方法先保存下绘图的状态,再调用 translate 后,绘制完图形后,调用 restore 方法恢复之前的上下文,对坐标系进行还原,这样不容易搞乱坐标系。

save方法通过将当前状态压入堆栈来保存画布的整个状态。

保存到堆栈上的图形状态包括:

  • 当前转换矩阵。
  • 当前裁剪区域。
  • 当前的破折号列表。
  • 包含的属性:strokeStyle、ill Style、lobalAlpha、linewidth、lineCap、lineJoin、miterLimit、lineDashOffset、shadowOffsetX、shadowOffsetY、shadowBlur、shadowColor、global alCompositeOperation、Font、extAlign、extBaseline、Direction、ImageSmoothingEnabled。

restore 方法通过弹出绘制状态堆栈中的顶部条目来恢复最近保存的画布状态。

ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 50, 50);// 保存绘图上下文
ctx.save()ctx.translate(50, 50);
ctx.fillStyle = 'green';
ctx.fillRect(50, 50, 50, 50);// 绘制完成后恢复上下文
ctx.restore()ctx.fillStyle = 'blue';
ctx.fillRect(0, 0, 50, 50);

translate3.png

scale 方法

语法:

scale(x, y)

缩放 (scale) 就是将一个图形围绕中心点,然后将宽和高分别乘以一定的因子(sx,sy)

默认情况下,画布上的一个单位正好是一个像素。缩放变换会修改此行为。例如,如果比例因子为0.5,则单位大小为0.5像素;因此,形状的绘制大小为正常大小的一半。类似地,比例因子为2会增加单位大小,使一个单位变为两个像素;从而以正常大小的两倍绘制形状。

举个例子:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');ctx.scale(0.5,2);
ctx.fillStyle="blue";
ctx.fillRect(50,50,100,50);

调用 scale(0.5,2) 将画布水平方向缩小一倍,垂直方向放大一倍,绘制一个坐标原点 (50, 50),宽度 100,高度 50 的矩形。经过缩放变换后,距离原点的实际像素是横轴 25像素,纵轴 100 像素,宽度 50 像素,高度 100 像素。

scale.png

实现鼠标拖动画布

效果

创建Sence类

Sence类:

class Scene {constructor(id, options = {width: 600,height: 400}) {this.canvas = document.querySelector('#' + id)this.width = options.width;this.height = options.height;this.canvas.width = options.width;this.canvas.height = options.height;this.ctx = this.canvas.getContext('2d');}draw() {this.ctx.fillStyle = 'red';this.ctx.fillRect(50, 50, 50, 50);this.ctx.fillStyle = 'green';this.ctx.fillRect(150, 150, 50, 50);}clear() {this.canvas.width = this.width;}paint() {this.clear();this.draw();}
}let scene = new Scene('canvas');scene.draw();

Sence 类的构造函数中初始化 Canvas,得到 CanvasRenderingContext2D 对象,并设置 Canvas 的宽高属性,draw 方法里面绘制了两个矩形。

在进行下面的工作之前,我们先来了解下 Canvas 的事件机制。

通过 addEventListener 方法可以给 Canvas 绑定一个事件。

this.canvas.addEventListener('mousedown', (event) => {console.log(event.x)
});

事件的回调函数参数的 event 对象中可以获取鼠标点击 Canvas 时的坐标信息,event 对象中经常会用到的坐标有两个,一个是 event.xevent.y,另一个是 event.offsetXevent.offsetY,其中,event.xevent.y 获取的是鼠标点击时相对于屏幕的坐标,而 event.offsetXevent.offsetY 是相对于 Canvas 容器的坐标。

通过下面这张图可以清晰的看出两个坐标的区别,明白这一点对于我们后续的坐标变换非常重要。

事件坐标系.png

在构造函数中添加对 Canvasmousedown 事件监听,记录点击鼠标时相对屏幕的位置 xy

class Scene {x = 0; // 记录鼠标点击Canvas时的横坐标y = 0; // 记录鼠标点击Canvas时的纵坐标constructor(id, options = {width: 600,height: 400}) {this.canvas.addEventListener('mousedown', this.onMousedown);}onMousedown(e) {if (e.button === 0) {// 点击了鼠标左键this.x = x;this.y = y;}}
}

画布拖动的整体思路就是利用前面介绍的 Canvastranslate 方法。画布的整体偏移量记录在 offset.xoffset.y,鼠标触发 mousedown 事件时,记录当前鼠标点击的位置相对于屏幕的坐标 x, 和 y,并且开始监听鼠标的 mousemovemouseup 事件。鼠标触发 mousemove 事件时计算每次移动时整体累加的偏移量:

onMousemove(e) {this.offset.x = this.curOffset.x + (e.x - this.x);this.offset.y = this.curOffset.y + (e.y - this.y);this.paint();
}

其中 curOffset.xcurOffset.y 记录的是鼠标触发 mouseup 时保存的当前的偏移量,便于计算累加的偏移量。每次触发完鼠标 mousemove 事件后,重新进行图形绘制。

onMouseup() {this.curOffset.x = this.offset.x;this.curOffset.y = this.offset.y;window.removeEventListener('mousemove', this.onMousemove);window.removeEventListener('mouseup', this.onMouseup);
}

Sence 类完整代码如下:

class Scene {offset = { x: 0, y: 0 }; // 拖动偏移curOffset = { x: 0, y: 0 }; // 记录上一次的偏移量x = 0; // 记录鼠标点击Canvas时的横坐标y = 0; // 记录鼠标点击Canvas时的纵坐标constructor(id, options = {width: 600,height: 400}) {this.canvas = document.querySelector('#' + id);this.width = options.width;this.height = options.height;this.canvas.width = options.width;this.canvas.height = options.height;this.ctx = this.canvas.getContext('2d');this.onMousedown = this.onMousedown.bind(this);this.onMousemove = this.onMousemove.bind(this);this.onMouseup = this.onMouseup.bind(this);this.canvas.addEventListener('mousedown', this.onMousedown);}onMousedown(e) {if (e.button === 0) {// 鼠标左键this.x = e.x;this.y = e.ywindow.addEventListener('mousemove', this.onMousemove);window.addEventListener('mouseup', this.onMouseup);}}onMousemove(e) {this.offset.x = this.curOffset.x + (e.x - this.x);this.offset.y = this.curOffset.y + (e.y - this.y);this.paint();}onMouseup() {this.curOffset.x = this.offset.x;this.curOffset.y = this.offset.y;window.removeEventListener('mousemove', this.onMousemove);window.removeEventListener('mouseup', this.onMouseup);}draw() {this.ctx.fillStyle = 'red';this.ctx.fillRect(50, 50, 50, 50);this.ctx.fillStyle = 'green';this.ctx.fillRect(150, 150, 50, 50);}clear() {this.canvas.width = this.width;}paint() {this.clear();this.ctx.translate(this.offset.x, this.offset.y);this.draw();}
}

上述代码中有几点需要注意:

  1. 事件函数中的this指向问题

细心的同学可能注意到,在 Sence 类的构造函数里有这样几行代码:

constructor(id, options = {width: 600,height: 400}) {this.onMousedown = this.onMousedown.bind(this);this.onMousemove = this.onMousemove.bind(this);this.onMouseup = this.onMouseup.bind(this);}

为什么要使用 bind 函数给事件函数重新绑定this对象呢?

主要的原因在于一个事件有监听就会有移除。假设我们想要销毁 mousemove 事件怎么办呢?

可以调用 removeEventListener 方法进行事件监听的移除,比如上述代码会在 onMouseup 中移除对 mousemove 事件的监听:

onMouseup() {this.curOffset.x = this.offset.x;this.curOffset.y = this.offset.y;window.removeEventListener('mousemove', this.onMousemove);
}

如果不在构造函数中使用 bind 方法重新绑定 this 指向,此时的 this 指向的就是window,因为 this 指向的是调用 onMouseup 的对象,而 onMouseup 方法是被 window 上的 mouseup 事件调用的,但是实际上我们想要的this指向应该 Sence 实例。为了避免上述问题的出现,最好的解决办法就是在 Sence 类的构造函数中重新绑定 this 指向。

  1. 画布的清空问题

每次鼠标移动的时候会改变 CanvasCanvasRenderingContext2D 偏移量,并重新进行图形的绘制,重新绘制的过程就是先将画布清空,然后设置画布的偏移量(调用 translate 方法),接着绘制图形。其中清空画布这里选择了重新设置Canvas的宽度,而不是调用 clearRect 方法,主要是因为clearRect 方法只在 Canvas 的渲染上下文没有进行过平移、缩放、旋转等变换时有效,如果 Canvas 的渲染上下文已经经过了变换,那么在使用 clearRect 清空画布前,需要先重置变换,否则 clearRect 将无法有效地清除整块画布。

实现鼠标滚轮缩放

效果

实现原理

鼠标滚轮的放大需要结合上面介绍的 Canvastranslatescale 两个方法进行组合变换。

计算放大系数

监听鼠标滚轮的 mousewheel 事件,在事件的回调函数中通过 event.wheelDelta 值的变化来实时计算当前的缩放值,其中 event.wheelDelta > 0 表示放大,反之表示缩小,放大和缩小都有对应的阈值,超过阈值就禁止继续放大和缩小。

改造 Sence 类,添加 onMousewheel 事件:

onMousewheel(e) {if (e.wheelDelta > 0) {// 放大this.scale = parseFloat((this.scaleStep + this.scale).toFixed(2)); // 解决小数点运算丢失精度的问题if (this.scale > this.maxScale) {this.scale = this.maxScale;return;}} else {// 缩小this.scale = parseFloat((this.scale - this.scaleStep).toFixed(2)); // 解决小数点运算丢失精度的问题if (this.scale < this.minScale) {this.scale = this.minScale;return;}}this.preScale = this.scale;
}

其中,this.scale / this.preScale 计算出来的值就是放大系数,暂且记做 n

在计算放大系数的时候,需要注意两个浮点型数值在计算不能直接相加,否则会出现丢失精度的问题。

缩放原理

在缩放的时候,会调用 scale(n, n) 方法,将坐标系放大 n 倍。假设鼠标滚轮停在 A 点进行放大操作,放大之后得到坐标 A’ 点。

图形放大1.png

可以看到,放大之后,A(x1, y1) 坐标变换到了 A'(x1, y1)A => A' 放大了 n 倍,因此得到 x1 = x * ny1 = y1 * n

这个时候就会存在一个问题,我们在 A 点进行放大,放大后得到的 A' 的位置应该是不变的,所以需要在放大之后需要调整 A’ 点的位置到 A 点。

这里我们采用的策略是在放大前先偏移一段距离,然后进行放大之后就可以保持 A 点和 A‘ 点的重合。

缩放原理图.png

鼠标停留在 A 点对蓝色矩形进行放大,放大系数为 n,蓝色矩形的起点左上角和坐标原点重合,宽度和高度分别是 xy,因此,A点的坐标为 (x, y)

前面我们说过,对 A 点进行放大后得到的 A’点应该和A点重合,这样就需要先把整个坐标系沿着x轴和y轴分别向左和向上偏移 offsetXoffsetY,偏移后得到的 A'点坐标记作 (x1, x2),因为 A 点是经过放大 n 倍后得到的 A' 点,所以得到以下距离关系:

x1 = x * n;
y1 = y * n

进一步就可以得到横纵坐标的偏移量 offsetXoffsetY 的绝对值:

offsetX = x*n-x;
offsetY =x*n - y;

因此,这需要将坐标系经过 translate(-offsetX, -offsetY) 之后,再 scale(n, n),就能确保 A 点 和 A‘ 点重合了。

明白了缩放的基本原理,下面就继续码代码吧😜。

onMousewheel(e) {e.preventDefault();this.mousePosition.x = e.offsetX; // 记录当前鼠标点击的横坐标this.mousePosition.y = e.offsetY; // 记录当前鼠标点击的纵坐标if (e.wheelDelta > 0) {// 放大this.scale = parseFloat((this.scaleStep + this.scale).toFixed(2)); // 解决小数点运算丢失精度的问题if (this.scale > this.maxScale) {this.scale = this.maxScale;return;}} else {// 缩小this.scale = parseFloat((this.scale - this.scaleStep).toFixed(2)); // 解决小数点运算丢失精度的问题if (this.scale < this.minScale) {this.scale = this.minScale;return;}}this.offset.x = this.mousePosition.x - ((this.mousePosition.x -   this.offset.x) * this.scale) / this.preScale;this.offset.y = this.mousePosition.y - ((this.mousePosition.y - this.offset.y) * this.scale) / this.preScale;this.paint(this.ctx);this.preScale = this.scale;this.curOffset.x = this.offset.x;this.curOffset.y = this.offset.y;
}paint() {this.clear();this.ctx.translate(this.offset.x, this.offset.y);this.ctx.scale(this.scale, this.scale);this.draw();
}

总结

本文从基础原理到代码实现,完整给大家讲解了 Canvas 画布绘制中经常会遇到的画布拖动和鼠标滚轮缩放功能,希望对大家有帮助,更多精彩文章欢迎大家关注我的vx公众号:前端架构师笔记。本文完整代码地址:https://github.com/astonishqft/scanvas-translate-and-scale

相关文章:

Canvas鼠标滚轮缩放以及画布拖动(图文并茂版)

Canvas鼠标滚轮缩放以及画布拖动 本文会带大家认识Canvas中常用的坐标变换方法 translate 和 scale&#xff0c;并结合这两个方法&#xff0c;实现鼠标滚轮缩放以及画布拖动功能。 Canvas的坐标变换 Canvas 绘图的缩放以及画布拖动主要通过 CanvasRenderingContext2D 提供的 …...

[ECCV 2020] FGVC via progressive multi-granularity training of jigsaw patches

Contents IntroductionProgressive Multi-Granularity (PMG) training frameworkExperimentsReferencesIntroduction 不同于显式地寻找特征显著区域并抽取其特征,作者充分利用了 CNN 不同 stage 输出的特征图的语义粒度信息,并使用 Jigsaw Puzzle Generator 进行数据增强来帮…...

Python推导式

列表&#xff08;list&#xff09;推导式 [remove for source in xx_list]或者[remove for source in xx_list if condition] 实例&#xff1a; names[Bob,Mark,Mausk,Johndan,Wendy] new_names[name.upper() for name in names if len(name)<5] print(new_names)即迭代列…...

Java列表List的定查改增删操作

Java列表List的定查改增删操作定义查找遍历元素与下标互查修改增加删除java.util中提供了三种常用的集合类&#xff0c;列表List、集合Map和字典Set。这些集合类相较于数组有更多功能&#xff0c;并且都可以通过Iterator&#xff08;迭代器&#xff09;来访问。 在这篇博客中&…...

day03java语言特性 JDK、JRE、JVM

1、Java语言的特性 1.1、简单性在Java语言当中真正操作内存的是&#xff1a;JVM&#xff08;Java虚拟机&#xff09;所有的java程序都是运行在Java虚拟机当中的。而Java虚拟机执行过程中再去操作内存。对于C或者C来说程序员都是可以直接通过指针操作内存的。C或者C更灵活&…...

HydroD 实用教程(二)有限元模型

目 录一、前言二、模型种类三、单元类型四、FEM文件五、参考文献一、前言 SESAM &#xff08;Super Element Structure Analysis Module&#xff09;是由挪威船级社&#xff08;DNV-GL&#xff09;开发的一款有限元分析&#xff08;FEA&#xff09;系统&#xff0c;它以 GeniE、…...

Java中的Set集合

Set不能存储重复元素&#xff0c;元素无序&#xff08;指的是不按照添加的顺序&#xff0c;List集合是按照添加顺序存储的&#xff09;hashSet注&#xff1a;源码底层是hashMap实现的&#xff0c;因为hashMap是双列的&#xff0c;其中键是不能重复的&#xff0c;而hashSet是单列…...

【RabbitMQ五】——RabbitMQ路由模式(Routing)

RabbitMQ路由模式前言RabbitMQ模式的基本概念为什么要使用Rabbitmq 路由模式RabbitMQ路由模式组成元素路由模式完整代码Pom文件引入RabbtiMQ依赖RabbitMQ工具类生产者消费者1消费者2运行结果截图前言 通过本篇博客能够简单使用RabbitMQ的路由模式。 本篇博客主要是博主通过官网…...

【C语言】宏定义 结构体 枚举变量的用法

目录 一、数据类型 二、C语言宏定义 三、C语言typedef重命名 四、 #define与typedef的区别 五、结构体 六、枚举变量 补充学习一点STM32的必备基础知识 一、数据类型 二、C语言宏定义 关键字&#xff1a;#define 用途&#xff1a;用一个字符串代替一个数字&#xff0c;…...

锁升级之Synchronized

Synchronized JVM系统锁一个对象里如果有多个synchronized方法&#xff0c;同一时刻&#xff0c;只要有一个线程去调用其中的一个synchronized方法&#xff0c;其他线程只能等待&#xff01;锁的是当前对象&#xff0c;对象被锁定后&#xff0c;其他线程都不能访问当前对象的其…...

基于nodejs+vue疫情网课管理系统

疫情网课也都将通过计算机进行整体智能化操作,对于疫情网课管理系统所牵扯的管理及数据保存都是非常多的,例如管理员&#xff1a;首页、个人中心、学生管理、教师管理、班级管理、课程分类管理、课程表管理、课程信息管理、作业信息管理、请假信息管理、上课签到管理、论坛交流…...

Zabbix 构建监控告警平台(三)

Zabbix User parametersZabbix Trigger1.Zabbix User parameters 1.1即自定义KEY 注意&#xff1a;mysql安装在被监测主机 [rootlocalhost ~]# yum -y install mariadb-server mariadb [rootlocalhost ~]# systemctl start mariadb [rootlocalhost ~]# mysqladmin -uroot statu…...

Linux系统之dool命令行工具的基本使用

Linux系统之dool命令行工具的基本使用一、dool命令行工具介绍二、本地系统环境检查1.检查系统版本2.检查系统内核版本三、下载dool软件包1.创建下载目录2.下载dool四、安装dool1.安装python32.安装dool五、dool的命令帮助六、dool的基本使用1.直接使用dool监控系统2.监控cpu和网…...

LeetCode-2335-装满杯子需要的最短总时长

1、堆 我们可以维护一个堆&#xff0c;首先我们将数组中不为0的数全部加入堆中&#xff0c;而后进行循环。当堆不为空时&#xff0c;我们将堆顶元素出堆并减一&#xff0c;而后观察是否还能继续出堆&#xff0c;若能则出堆&#xff0c;否则跳过&#xff0c;最后我们将处理后的…...

npm ERR! code ELIFECYCLE解决方案,npm犯错!myweb@1.0.0构建脚本失败。

1.问题npm ERR! code ELIFECYCLEnpm ERR! errno 1npm ERR! myweb1.0.0 build: webpack --config config/webpack.config.jsnpm ERR! Exit status 1npm ERR!npm ERR! Failed at the myweb1.0.0 build script.npm犯错!代码ELIFECYCLEnpm犯错!errno 1npm犯错!myweb1.0.0 build: we…...

最小二乘支持向量机”在学习偏微分方程 (PDE) 解方面的应用(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 本代码说明了“最小二乘支持向量机”在学习偏微分方程 &#xff08;PDE&#xff09; 解方面的应用。提供了一个示例&#xff0c…...

ISYSTEM调试实践8-winIDEA Analyzer功能1

前面几篇介绍了ISYSTEM的基本调试界面和功能&#xff0c;相比我之前用过的IDE&#xff0c;除了几种断点方式和脚本功能以外&#xff0c;应该都是比较简单&#xff0c;稍微操作一下就可以直接上手&#xff0c;后续我将介绍winIDEA的Analyzer 功能。 1 Analyzer简介 iSYSTEM An…...

每日学术速递2.11

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.IR、cs.MM 1.A Comprehensive Survey on Multimodal Recommender Systems: Taxonomy, Evaluation, and Future Directions 标题&#xff1a;关于多模态推荐系统的综合调查&#xff1a;分…...

宝塔搭建实战php开源likeadmin通用管理admin端vue3源码(二)

大家好啊&#xff0c;我是测评君&#xff0c;欢迎来到web测评。 上一期给大家分享了server端的部署方式&#xff0c;今天来给大家分享admin端在本地搭建&#xff0c;与打包发布到宝塔的方法。感兴趣的朋友可以自行下载学习。 技术架构 vscode node16 vue3 elementPlus vit…...

网络基础-虚拟化工具-网桥

系列文章目录 本系列文章主要是回顾和学习工作中常用的网络基础命令&#xff0c;在此记录以便于回顾。 该篇文章主要是讲解虚拟化的工具网桥相关的概念和常用命令 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录系…...

Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误

HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误&#xff0c;它们的含义、原因和解决方法都有显著区别。以下是详细对比&#xff1a; 1. HTTP 406 (Not Acceptable) 含义&#xff1a; 客户端请求的内容类型与服务器支持的内容类型不匹…...

rknn优化教程(二)

文章目录 1. 前述2. 三方库的封装2.1 xrepo中的库2.2 xrepo之外的库2.2.1 opencv2.2.2 rknnrt2.2.3 spdlog 3. rknn_engine库 1. 前述 OK&#xff0c;开始写第二篇的内容了。这篇博客主要能写一下&#xff1a; 如何给一些三方库按照xmake方式进行封装&#xff0c;供调用如何按…...

Zustand 状态管理库:极简而强大的解决方案

Zustand 是一个轻量级、快速和可扩展的状态管理库&#xff0c;特别适合 React 应用。它以简洁的 API 和高效的性能解决了 Redux 等状态管理方案中的繁琐问题。 核心优势对比 基本使用指南 1. 创建 Store // store.js import create from zustandconst useStore create((set)…...

第25节 Node.js 断言测试

Node.js的assert模块主要用于编写程序的单元测试时使用&#xff0c;通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试&#xff0c;通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...

AI编程--插件对比分析:CodeRider、GitHub Copilot及其他

AI编程插件对比分析&#xff1a;CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展&#xff0c;AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者&#xff0c;分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...

让AI看见世界:MCP协议与服务器的工作原理

让AI看见世界&#xff1a;MCP协议与服务器的工作原理 MCP&#xff08;Model Context Protocol&#xff09;是一种创新的通信协议&#xff0c;旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天&#xff0c;MCP正成为连接AI与现实世界的重要桥梁。…...

基于江科大stm32屏幕驱动,实现OLED多级菜单(动画效果),结构体链表实现(独创源码)

引言 在嵌入式系统中&#xff0c;用户界面的设计往往直接影响到用户体验。本文将以STM32微控制器和OLED显示屏为例&#xff0c;介绍如何实现一个多级菜单系统。该系统支持用户通过按键导航菜单&#xff0c;执行相应操作&#xff0c;并提供平滑的滚动动画效果。 本文设计了一个…...

Qt Quick Controls模块功能及架构

Qt Quick Controls是Qt Quick的一个附加模块&#xff0c;提供了一套用于构建完整用户界面的UI控件。在Qt 6.0中&#xff0c;这个模块经历了重大重构和改进。 一、主要功能和特点 1. 架构重构 完全重写了底层架构&#xff0c;与Qt Quick更紧密集成 移除了对Qt Widgets的依赖&…...

AWS vs 阿里云:功能、服务与性能对比指南

在云计算领域&#xff0c;Amazon Web Services (AWS) 和阿里云 (Alibaba Cloud) 是全球领先的提供商&#xff0c;各自在功能范围、服务生态系统、性能表现和适用场景上具有独特优势。基于提供的引用[1]-[5]&#xff0c;我将从功能、服务和性能三个方面进行结构化对比分析&#…...

npm安装electron下载太慢,导致报错

npm安装electron下载太慢&#xff0c;导致报错 背景 想学习electron框架做个桌面应用&#xff0c;卡在了安装依赖&#xff08;无语了&#xff09;。。。一开始以为node版本或者npm版本太低问题&#xff0c;调整版本后还是报错。偶尔执行install命令后&#xff0c;可以开始下载…...