先来看一个好玩的gif演示:

通过编写JSDom脚本,控制PhotoShop,最终自动进行九宫格切图,并且将整个切图过程输出到文件:

本文涉及的主题是PhotoShop plugin开发的基础,主要关注如何在PS中自己来绘制形体。

要做的:

基础绘制篇--以PhotoShop的Document为舞台,在上面绘制文字和任意形状

不做的:

1) 不涉及Channel操作,对位图像素操作我们不关心
2) 不涉及PhotoShop界面编程。因为PS界面编程随着版本变化,有好多种方式,一直在改变。C++方式,强大,但是难度较大,不是三言两语说得清楚的As3 Flex方式,简单易用,和PSDom交互方便,但是适合Adobe CS系列Html5方式,简单应用,和PSDom交互方便,但是适合Adobe CC系列但不管如何,核心的PhotoShop操作是通过PSDom API公开出来的,这个不管版本如何变,其本身DOM是不会发生较大变化的。因此才更有研究的价值看过本文后,你会发现其实开发PS插件蛮简单的,PSDom非常强大。

PSDom的开发环境—ExtendScript ToolKit
在安装Adobe cs/cc系列时,会自动安装(我装的是CS5.0)

如果在Windows下开发,请见后缀名改为.jsx
如果使用.js后缀的话,可能会由Windows Scripting Host(WSH)来执行,而不是PS 脚本解析器。防冲突!

通过这篇文章,希望能够让大家在PhotoShop中为所欲为!!!

让我们来关注一下吧:

1)PS DOM(Photoshop 文档对象模型)用来操纵PS中的各个对象,可以使用AppleScript(mac专用),VBScript(windows专用),以及JavaScript(跨平台,本文档仅使用js代码来演示).

2)PS DOM中关键类图(竖线表示包含关系,且第三层开始的所有类都是所属于Document):

3)与绘制相关的对象:
  二维绘制可以归类为三个绘制范畴:

1、位图/图像操作:
    PS Dom通过Application.Document.Channel(通道)对象进行某个通道的数据操作(例如你可以分别对位图的四个通道R G B A 进行操作),这不是本文所关注的。

2、文本绘制:
    PS Dom通过使用Application.Document.ArtLayer.TextItem进行文本的显示和相关操作。
    由上面的寻址关系可以看到TextItem文本对象是被附加到PS中的一个Layer上的

3、矢量绘制:
    PS Dom 通过使用:
Application.Document.PathItem
Application.Document.PathItem.SubPathItem                 Application.Document.PathItem.SubPathItem.PathPoint
    这三个类来进行相关操作的(实际上还有其他一些辅助类,具体我们在后面了解)
    由上面的寻址关系可以看到PathItem路径对象是并没有像TextItem一样是被附加到PS中的一个Layer上的,而是独立于layer对象的

4)JS运行调试环境和API参考文档:
  安装好PhostShop CS5或更高的版本后,在你的PS安装目录下可以看到如下目录:


其中JavaScript Ref和Scripting Guide是你必须要进行查阅的参考,因为PS DOM开发网络上的信息非常少,所以这两篇文档是你基本唯一的依靠

Utilities 目录下:

scriptListener插件,用于PS Action录制时候将录制的步骤的js代码记录下来并输出到桌面文件。 是一个非常重要的具有开发功能插件,以后会有专门文章,目前不关注。

Sample Scripts 目录下:
双击运行

里面都是js演示代码:

双击后跳出Yes/No Alert界面:
Yes会打开PS并运行代码,显示脚本运行后的效果
No会打开JS开发环境,可以进行脚本开发,修改,运行,debug等

5)常用的基础代码结构流程:

1、js代码文件的开始

   //每个JS文件的开始部分,这些代码基本可以说是固定的// 如果有 #target photoshop,就直接打开Photoshop运行js脚本//如果没有,则打开ps的脚本编辑器而不是直接在Photoshop中运行#target photoshop// in case we double clicked the fileapp.bringToFront();

2、单位标尺等数据的保存与恢复:

   // 存储当前的状态var startRulerUnits = app.preferences.rulerUnits;var startTypeUnits = app.preferences.typeUnits;var startDisplayDialogs = app.displayDialogs;//设置新的状态,我们使用像素来作为标尺和文档中各个对象的计量单位//当然也可以设置其他计量单位,例如厘米,英寸等app.preferences.rulerUnits = Units.PIXELS;app.preferences.typeUnits = TypeUnits.PIXELS;app.displayDialogs = DialogModes.NO;//之所以这么做,是因为ps的开发是基于状态机模式的
//所有图形化操作基本都是状态机,opengl d3d gdi quartz2d.......

3、如果你PS打开了很多程序,希望只显示现在正在操作的那个文档,那么可以使用下面代码先关闭所有文档

    // 关闭所有打开的文档while (app.documents.length != 0) {app.activeDocument.close();}

4、使用open全局函数,打开一个PSD文件,并将该文件设置为当前活动的Document

     var  doc = open(File(app.path + "/示例/mytest.psd"));app.activeDocument = buttonDoc;

5、 针对各种文档进行相关的操作

针对文档对象以及文档中的各个对象进行操作的代码是需要你来实现的!!

6、操作完文档后,需要恢复到原来的状态(恢复到开始时记录下来的状态值)

     // 恢复到修改前的状态app.preferences.rulerUnits = startRulerUnits;app.preferences.typeUnits = startTypeUnits;app.displayDialogs = startDisplayDialogs;//状态机恢复default状态

6) 文本操作(TextItem)
  需求描述:
  1、打印出ps所有的Font对象的信息,目的是为了了解TextFont对象主要属性: Family|name|postScriptName|style

   function printAllInstalledTextFontInfo(){var allFonts = [];for(var i = 0; i < app.fonts.length; i++){var str = "{"+app.fonts[i].family+"|"+app.fonts[i].name+"|"+app.fonts[i].postScriptName+"|"+app.fonts[i].style+"}";allFonts.push (str);}alert(allFonts);}

2、不要从现有的PSD载入,而是从无到有通过代码创建文档,层等

   //创建一个Document对象var docRef = app.documents.add(400, 300, 72);//创建一个ArtLayer对象var newTextLayer = docRef.artLayers.add();//注意:一个文本对象必须要依附于一个Layer对象,并且Layer的kind必须是TEXT类型newTextLayer.kind = LayerKind.TEXT;

3、使用微软雅黑并blod字体样式

   //当设置为TEXT类型时,PS自动会创建一个TextItem对象给Layer,因此可以直接引用newTextLayer.textItem.font = "MicrosoftYaHeiUI-Blod";//这里需要注意的是,TextItem.font的数据类型是字符串,该字符串是使用了TextFont//对象中的postScriptName,这一点务必要注意。//由这里看出,前面 printAllInstalledTextFontInfo函数的作用了,你可以打印出来,查找//postScriptName的名称,然后记住是用这个名称来设置字体name和style

4、绘制文字内容为”随风而行的PSDOM Demo”

   //设置要显示的内容newTextLayer.textItem.contents = "随风而行的PSDOM Demo";

5、设置文字的大小

   newTextLayer.textItem.size = 36;

6、文字颜色为红色(SolidColor对象)

   var textColor = new SolidColor;textColor.rgb.red = 255;textColor.rgb.green = 0;textColor.rgb.blue = 0;newTextLayer.textItem.color = textColor;//这样就可以显示文字了

其他的文字相关属性请查阅文档

7)路径操作(PathItem)—-核心操作

1、先来一段示例代码,能够对PathItem有个比较直观的了解
     我们编写一个函数,用来显示多边形:

   function DrawPolygon() {//PS是状态机,基于当前选中的状态进行操作var doc = app.activeDocument;//获取参数的数量,使用js可变参数
//因为多边形参数不确定,例如三角形,三个顶点,n边形,n个顶点
var y = arguments.length;
var i = 0;var lineArray = [];
for (i = 0; i < y; i++) {//创建一个PathPointInfo对象//里面包含绘制点的相关信息lineArray[i] = new PathPointInfo;//多边形是凸包,没有任何曲线段表示,因此每个点都是CORNERPOINT类型//如果是曲线的话,那么每个点的类似是SMOOTHPOINTlineArray[i].kind = PointKind.CORNERPOINT;//要绘制的点的坐标,来源于参数,类型为[x,y];//对于非曲线来说,leftDirection = rightDirection=anchorlineArray[i].anchor = arguments[i];lineArray[i].leftDirection = lineArray[i].anchor;lineArray[i].rightDirection = lineArray[i].anchor;
}//到此处,所有的绘制点的信息都保存在lineArray数组中//创建一个SubPathInfo对象
var lineSubPathArray = new SubPathInfo();//SubPathiInfo.entireSubPath指向了要绘制的顶点数据数组lineSubPathArray.entireSubPath = lineArray;//设置SubPathiInfo.closed为true,这样在strokePath时候,会自动封闭整个路径
//否则如果为false的话,那么会缺少最后一条线段,导致路径非封闭状态。
lineSubPathArray.closed = true;//设置ShapeOperation为Add模式,叠加模式,前景层直接覆盖到背景层上
//还有其他也写操作,可以理解为布尔操作,例如前景和背景取并集,交集,差集等
lineSubPathArray.operation = ShapeOperation.SHAPEADD;//创建一个PathItem对象,使用的是doc.pathItems.add方法
//注意,我们会发现是doc而不像TextItem是属于层对象的。
var myPathItem = doc.pathItems.add("myPath" , [lineSubPathArray]);//调用PathItem的描边函数
//矢量图形绘制可以分为边的绘制以及封闭形体的填充两种操作
//strokePath用来进行边的绘制
//fillPath则用来进行填充内容
myPathItem.strokePath(ToolType.PENCIL);//绘制好后,将PathItem去除掉,由于已经描边渲染了,所有所有效果都输出到
//像素缓冲区了,因此不需要该PathItem了
//如果你需要后续进行顶点级别的操作的话,那你也可以保留着,不要remove掉myPathItem.remove();
}

2、测试多边形绘制:

//从两个点生成4个绘制点,绘制Rect
function DrawRect(left,top,right,bottom)
{DrawPolygon( [left,top], [right,top], [right,bottom], [left,bottom] );
}//由于strokePath时使用的颜色是基于当前的前景色的
//注意,如果是填充封闭路径fillPath的话,则使用指定颜色作为参数,但是描边是基于前
//景色的操作
//为了防止干扰,因此先记录下当前的前景色
var   saveColor = app.foregroundColor;//生成一个红色的SolidColor对象
var newColor = new SolidColor;
newColor.rgb.red = 255;
newColor.rgb.green = 0;
newColor.rgb.blue = 0;//设置前景色为红色,并绘制三角形
app.foregroundColor = newColor;
DrawPolygon([250,10],[350,10],[250,100]);//修改颜色为绿色
newColor.rgb.red = 0;
newColor.rgb.green = 255;
newColor.rgb.blue = 0;//设置前景色为绿色,并绘制四边形
app.foregroundColor = newColor;
DrawRect(10,100,100,200);//修改颜色为蓝色,绘制8角形
newColor.rgb.red = 0;
newColor.rgb.green = 0;
newColor.rgb.blue = 255;
app.foregroundColor = newColor;
DrawPolygon([36.9999885559082,13.9999985694885],[165.99999666214,13.9999985694885],[185.999989509583,33.9999973773956],[185.999989509583,61.9999945163727],[165.99999666214,81.999×××847443],[36.9999885559082,81.999×××847443],[16.9999957084656,61.9999945163727],[16.9999957084656,33.9999973773956]);//完成后,将前景色恢复到以前记录下来的颜色
app.foregroundColor = saveColor;

在PhotoShop显示的结果如下:(我们使用Brush进行绘制,如果线条的话,可以使用Pencil)

如果使用填充模式,在PhotoShop中获得的效果:

3、PathItem对象模型:

由此可见,PathItem对象的读写是使用不同的类来表示的,切记!!

4、贝塞尔曲线曲面研究:
pathItem中的PathPoint有三个很重要的属性

pathPoint.anchor[x,y]          The X and Y coordinates of the anchor point of the curvepathPoint.leftDirection[a,b]   The location of the left-direction endpoint (’in’position). pathPoint.rightDirection[e,f]  The location of the right-direction endpoint (’out’position).

For paths that are straight segments (not curved), the coordinates of all three points are the same.
如果是直线的话,leftDirection = rightDirection = anchor

但是如果是曲线的话,文档中的解释是
For curved segements, the the coordinates are different. The difference between the anchor point and the left or right direction points determines the arc of the curve. You use the left direction point to bend the curve “outward” or make it convex; you use the right direction point to bend the curve “inward” or make it concave.
这些描述非常抽象,而且ps dom文档只演示了非曲线曲面的demo,对于曲线曲面并没有demo,而且网上也很难查到相关资料

因此只有抽取数据进行查看了解其数据的格式以及猜测strokePath是如何绘制的

为了研究strokePath是如何绘制曲线的,必须先要取得矢量对象中所有曲线的点,看看是如何组成的,所以先将矢量对象所有的点输出到文件,以利于查看:

function saveFile(msg1,msg2,msg3,types)
{//弹出saveFile对话框,save类型为txt文件var file = File.saveDialog("Saving TXT file.", "TXT Files: *.txt");if ( file == null ) return;//如果已经存在,弹框确认是否覆盖重写if (file.exists) {if(!confirm(file.fsName + " exists.\\\\rOver write?")) return;}//打开要写的文件流,并且设置编码为utf16格式存储file.open("w"); // open as writefile.encoding = "UTF16";file.write("\\\\uFEFF"); // Unicode marker//将anchor数据写入文件流file.write("pts:");file.write(msg1);//将left Direction points数据写入文件流file.write("lts:");file.write(msg2);file.write("rts:");file.write(msg3);//将right Direction points数据写入文件流file.write("tps:");file.write(types);file.write("\\\\n");//关闭文件流,写入完成file.close();
}
//将每个pathItem中的每个subPathItem中的pathPoint值输出到文件参看具体数据
for(var i = 0;  i < buttonDoc.pathItems.length; i++)
{   var subItems = buttonDoc.pathItems[i].subPathItems;for(var j = 0; j < subItems.length; j++){var subItem = subItems[j];var  pathPoints = subItem.pathPoints;var  pts = [];var  lefts = [];var  rights = [];var  types = []for(var k = 0; k < pathPoints.length; k++){pts.push(pathPoints[k].anchor);lefts.push(pathPoints[k].leftDirection);rights.push(pathPoints[k].rightDirection);types.push(pathPoints[k].kind);}saveFile(pts,lefts,rights,types);}
}上面代码运行后会在桌面生成一个文件,例如叫PathItem.txt

我们将一个圆角矩形输出到文件,并且对浮点数进行四舍五入后获得所有数据,进行分析:

1、顶点的定义是左手系,顺时针方式 [0,1,2,3,4,5,6,7]如上图所画形状。多边形的面是有正反面之分,ps中按照左手顺时针为正面。2、线段是以Line_Strip或Line_Strip_Loop方式连接的

上图来源于opengl的图元类型中的线图源类型
ps使用的是:

Line_Strip(如果SubPathItem.closed = false)
Line_Strip_loop(如果SubPathItem.closed = true).

备注,OpenGL的顶点定义是右手系逆时针。而psdom中和d3d一样使用左手系表示

Line_Strip            n个顶点确定n-1条线段   n >= 2
Line_Strip_Loop       n个顶点确定n条线段,最后一条线段首位相连 n>=2

3、PathItem贝塞尔3次曲线中四个顶点的含义

如果要绘制上面的曲线,则顶点如下:
pathPoint0.anchor = p0;
pathPoint0.leftDirection = p0;
pathPoint0.rightDirection = r0;pathPoint1.anchor = p1;
pathPoint1.leftDirection = l1;
pathPoint1.rightDirection = r1;pathPoint1.anchor = p2;
pathPoint1.leftDirection = l2;
pathPoint1.rightDirection = p0;

具体还是来看一个完整的绘制圆角矩形的例子吧

function DrawRoundRetange(left,top,right,bottom,radius)
{if(radius <= 0.01){DrawRetangle(left,top,right,bottom);return;}if(radius>=(Math.min(right-left, bottom-top))/2.0){alert("半径太大");//如何处理,要研究//PS中、一般的情况下,是8个点//但是在半径太大或者其他一些情况下,貌似用6个点//目前还没法逆向出来,以后再研究吧!!return; }var doc = app.activeDocument;var i = 0;var lineArray = [];//下面代码以半径和顶点为参数,设置锚点和定位点//具体算法上图演示//这些算法需要有一定数学基础的,我正想开辟一个文章集,名为//数学之美lineArray[0] = new PathPointInfo;lineArray[0].kind = PointKind.SMOOTHPOINT;lineArray[0].anchor = [left+radius,top];lineArray[0].leftDirection =lineArray[0].anchor;lineArray[0].rightDirection = [left,top];lineArray[1] = new PathPointInfo;lineArray[1].kind = PointKind.SMOOTHPOINT;lineArray[1].anchor = [right-radius,top];lineArray[1].leftDirection = [right,top];lineArray[1].rightDirection = lineArray[1].anchor;lineArray[2] = new PathPointInfo;lineArray[2].kind = PointKind.SMOOTHPOINT;lineArray[2].anchor = [right,top+radius];lineArray[2].leftDirection =  lineArray[2].anchor;;lineArray[2].rightDirection = [right,top];lineArray[3] = new PathPointInfo;lineArray[3].kind = PointKind.SMOOTHPOINT;lineArray[3].anchor = [right,bottom-radius];lineArray[3].leftDirection = [right,bottom];lineArray[3].rightDirection = lineArray[3].anchor;lineArray[4] = new PathPointInfo;lineArray[4].kind = PointKind.SMOOTHPOINT;lineArray[4].anchor = [right -radius,bottom];lineArray[4].leftDirection = lineArray[4].anchor;lineArray[4].rightDirection =[right,bottom];lineArray[5] = new PathPointInfo;lineArray[5].kind = PointKind.SMOOTHPOINT;lineArray[5].anchor = [left+radius,bottom];lineArray[5].leftDirection = [left,bottom];lineArray[5].rightDirection = lineArray[5].anchor;lineArray[6] = new PathPointInfo;lineArray[6].kind = PointKind.SMOOTHPOINT;lineArray[6].anchor = [left,bottom-radius];lineArray[6].leftDirection = lineArray[6].anchor;lineArray[6].rightDirection = [left,bottom];lineArray[7] = new PathPointInfo;lineArray[7].kind = PointKind.SMOOTHPOINT;lineArray[7].anchor = [left,top+radius];lineArray[7].leftDirection = [left,top];lineArray[7].rightDirection = lineArray[7].anchor;var lineSubPathArray = new SubPathInfo();lineSubPathArray.closed = true;lineSubPathArray.operation = ShapeOperation.SHAPEADD;lineSubPathArray.entireSubPath = lineArray;var myPathItem = doc.pathItems.add("myRoundedRetangle" , [lineSubPathArray]);myPathItem.strokePath(ToolType.PENCIL);//myPathItem.fillPath ();myPathItem.remove();
}

我们来测试一下:

// 存储当前的状态
var startRulerUnits = app.preferences.rulerUnits;
var startTypeUnits = app.preferences.typeUnits;
var startDisplayDialogs = app.displayDialogs;//设置新的状态
app.preferences.rulerUnits = Units.PIXELS;
app.preferences.typeUnits = TypeUnits.PIXELS;
app.displayDialogs = DialogModes.NO;// 关闭所有打开的文档
while (app.documents.length != 0) {app.activeDocument.close();
}//创建一个新的500*500大小 dpi=72的文档对象,名称为PathItemTest
//并且设定为当前对象
var doc = app.documents.add(500,500,72, "PathItemTest");
app.activeDocument = doc;//调用上面的函数
drawRoundRetange(20,20,200,200,50);//恢复为系统初始化的状态
app.preferences.rulerUnits = startRulerUnits;
app.preferences.typeUnits = startTypeUnits;
app.displayDialogs = startDisplayDialogs;

至此,我们掌控Photoshop的第一篇结束,以后有机会我们来关注如何进行PS Layer的操作,这也是非常有趣的一个话题。

上图photoshop中的界面使用flash as3.0编写,目前html5是最好的替代品

注: 关于参数曲线曲面,其实还是有很多数学知识的,包括前段时间发的OpenGL太阳系Demo这篇bolg,并没有很详细的讲解代码,因为都涉及到很多计算机图形学的数学方法,因此随风计划开辟一个名为数学之美的文集,使用基于javascript /typescript的Canvas2D,WebGL来演示各种动画效果,体验数学之美!

转载于:https://blog.51cto.com/jackyblf/1892827

让我们掌控Photoshop吧!相关推荐

  1. 云米冰箱能控制扫地机器人_用冰箱就能掌控全屋家电?云米21Face 428L确实可以...

    现代年轻人都很懂时尚科技,他们都很懂利用智能手机来智能扩展更多的玩法.然而对于"宅"在家中时,掌控全屋家电已经无需拿起手机那么麻烦,只需面对着云米21Face互联网冰箱 428L, ...

  2. 深度强化学习的前景:帮助机器掌控复杂性

    作者:数据实战派 来源:数据实战派 深度强化学习,即机器通过测试其行为后果来学习的方法,是人工智能最有前途和影响力的领域之一.它将深度神经网络与强化学习结合在一起,可以通过训练实现多个步骤的目标. 它 ...

  3. 技术总监的反思录:我是如何失去团队掌控的?

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 作者 | zer0black 来源 | https://w ...

  4. mpython掌控板作品_mPython掌控板Easy-IoT物联

    [原由] 10月底,在做一个项目时用SioT物联掌控,发现了一个小问题,总是不稳定. 以前也用过SIoT和掌控板来做物联,没有感觉到问题啊,难道在测试时总是通了就好,没有做稳定性测试么? 和谢老师进行 ...

  5. 趋势科技全球首席安全官ED:人类迈向智能社会进程中不能失去掌控力

    如果你还对<速度与激情8>中大反派塞弗控制几百上千辆自动驾驶汽车演绎"僵尸车"."汽车雨"的街头较量的画面感到印象深刻,那么你总该相信这并非完全杜撰 ...

  6. 青源LIVE第29期|清华叉院高阳:使用1/500数据掌控Atari游戏-EfficientZero算法详解

    当前强化学习已在许多应用中取得了巨大成功.但样本效率仍是强化学习中一个重大挑战,重要的方法需要数百万(甚至数十亿)的环境步骤来训练.虽然,当前在基于图像的样本高效RL算法方面取得了重大进展:但是,在A ...

  7. 一文读懂深度学习:这个AI核心技术被美国掌控,很危险

    2019-11-28 18:39:22 "中国有多少数学家投入到人工智能的基础算法研究中?" 今年4月底,中国工程院院士徐匡迪等多位院士的发声,直击我国在算法这一核心技术上的缺失, ...

  8. 多巴胺如何驱使我们克服复杂情况、逆境、情绪, 让我们掌控周遭的环境的

    来源:本文摘自<贪婪的多巴胺> 仅仅是"想要"很少能让你得到任何东西.你必须弄清楚如何获得它,以及它是否值得拥有.事实上,如果我们做事时不考虑怎么做和下一步做什么,失败 ...

  9. 3星|《掌控大趋势》:全球当前政治经济形式的资料汇编整理,对未来的预测很少,也缺乏独到深刻的见解...

    掌控大趋势:如何正确认识.掌控这个变化的世界 全书基本是当前全球政治经济形式的公开资料的汇编整理,对未来的预测比较少.个人看法书中也缺乏独到或深刻的见解. 总体评价3星,有一些参考价值. 以下是书中一 ...

最新文章

  1. 2分钟学会ajax 入门ajax必备
  2. Unity基础之:UnityAPI的学习
  3. 微信语音麦克风静音_微信聊天打字慢?教你一分钟打出200个字,父母老人都需要...
  4. Objective-C 深复制和浅复制与NSCopying协议
  5. html无序列表中的正方形点点,CSS 有序或者无序列表的前面的标记 list-style-type 属性的实现...
  6. 解决苹果手机点击返回键页面不刷新问题
  7. 比较完善的手写数组reduce方法
  8. 【托马斯微积分】(12版)阅读笔记1:函数
  9. 中兴 F607 光猫超级管理用户查看
  10. mysql实验报告4_数据库实验四实验报告
  11. 多款日志分析工具,总有一款适合你
  12. 百词斩不认识单词例句导出
  13. Linux 8723be无线网卡,rtl8723be无线网卡驱动 for ubuntu
  14. mind map 思维导图
  15. focus和onfocus区别
  16. Java: 实训三 类与对象、继承
  17. 微信中将时间戳转换为聊天时间(二)
  18. 利用Python做一个漂亮小姐姐词云跳舞视频
  19. python 小工具 之 房贷计算器
  20. Java实现swap交换函数的数组方法

热门文章

  1. 阿里旺铺装修的主要区域在哪里
  2. 本次安装Lion记录
  3. 个人软件开发知识体系梳理
  4. 2021年中国钴产量、消费量、进出口、价格走势及竞争格局分析「图」
  5. FPGA时序约束理论(基于Vivado)
  6. Java学习之路4——Java数组
  7. 三菱FX1s与3台台达MS300变频器通讯程序
  8. CAD二次开发(Vba)------SETXDATA使用
  9. jar包中的声明文件
  10. mysql on update on delete_MySQL外键约束On Update和On Delete的使用说明