- 浏览: 55545 次
- 性别:
- 来自: 北京
最新评论
-
fjjiaboming:
求指教.求指教.
JAVA InputMethod 输入法实现纪要 -
fjjiaboming:
能不能详细点??????????想写一写输入法...
JAVA InputMethod 输入法实现纪要 -
mayi140611:
高深,学习中,
Java dnd拖拽实现分析纪要
JAVA Painting-Swing实现纪要三
前两节实现了Swing的绘制机制,即在整个GUI应用中所有的绘制时机是怎么产生的,时机产生后又怎么样经过swing框架的基础处理最终进入合适组件开展具体paint。那么每个组件在得到绘制时机时,如何进行绘制?这就从计算机显示世界的历史开始说起了。
在很久很久以前老一辈革命家是通过INT10直接向显示区内存书写显示字节数据,显示芯片会将这些数据按频率生成模拟信号提供显示器进行显示。此后发生了很多方面的进化,一方面是语言上有了c,c++,所以我们不再直接int10,而是调用其API函数;另一方面是绘制的各个方面被封装化,不再直接画点像素,而是逐渐抽象封装了很多绘制工具包,即如果要绘制一个立方体,将有函数调用,提供其顶点位置即可;再一方面则是硬件开发参与进来,为了提高性能,很多软件上的实现,比如当调整角度时一个立方体的显示将发生像素的变化,这个计算被转移到硬件上实现。这些方面的进化是同步展开的:首先说windows为了隔离不同主机的各类厂商不同的显卡其不同的指令约定,提供了GDI统一API供应用程序调用,windows底层将利用各类厂商的驱动来完成映射,此后windows在.net框架上以GDI+的形式包装了GDI以提供OO的调用形式;于此同时,面对那些对绘制性能要求非常高的应用程序,尤其是3D游戏,为了能够让开发的程序更加迅速地绘制,windows提供了Direct API工具包,其调用绕过了许多环节,迅速操作显卡的显示内存区来完成绘制。Direct因其(for speed)使命决定了此后的由软件设计商来主导硬件开发商设计的历史。随着3D绘制需求日益膨胀,direct不断升级版本,提供了很多更高级的调用函数封装来简化开发,这不仅仅是可调用函数的数量增长,而且是提出了适应开发需求的新的绘制体系。只有全面支持direct的硬件厂商才有更好的出路,于是他们纷纷追寻direct的基调进行硬件设计以实现direct显示加速。到了现代,在windows体系里,如果要开发一般的GUI,则GDI/GDI+是合适的选择,虽然GDI只有一部分可能借助硬件加速,GDI+则完全没有硬件加速,但是应付一般的GUI绰绰有余,关键这是普适任何环境的;如果要开发3D游戏或其他图形应用,则direct是合适的选择,虽然如果主机没有direct显卡将可能无法运行,但是现在的显卡绝大多数都支持direct;而最近microsoft在.net3推出的WPF,则是利用direct3d封装及xml描述来提供统一绘制API。在之前提到的windows启动桌面窗口管理器(Desktop Window Manager,DWM)技术后java2d将默认不使用双缓冲,因为DWM自身实现了窗口双缓冲机制,这个dwm就是建立在WPF基础之上。
Java的想法是在各操作系统上封装其底层图形API提供一个统一的图形绘制工具包,不管是2d还是3d,最终利用java绘制时将把调用转换为操作系统的底层图形进行调用。在Windows平台下要进行绘制,首先要打开一个窗口,而后的底层绘制api调用都要跟这个窗口相关(以窗口作为调用的一个参数或者内置为一个成员变量,从而可以在操作系统层面控制各个应用程序的统一表现,比如防止某个应用程序去绘制另外一个应用程序的窗口),这个设计原则适用于各类窗口操作系统。那么在java swing应用程序中,当打开顶层容器时,一个wwindowpeer窗口对等体被构造,这就对应了底层的一个window窗口,在peer构造方法中将会对此peer作为构造参数包出一个顶层容器的SurfaceData屏面成员对象。此后当绘制时机到来,经框架层层过滤确定要进行本容器内任何一个组件的paint时,框架会追溯组件的祖宗树,最终回到该顶层容器里safelyGetGraphics,这里将会构造一个new Graphics对象,同时将此顶层容器的SurfaceData屏面作为该Graphics对象的构造参数。经过以上的设计,组件的paint中使用safelyGetGraphics过来的Graphics开展的各种draw,都可在具体落实到屏幕时找到对应的底层窗口从而有效地转换成底层windows绘制api完成绘制。
Graphics2D是Graphics的加强版实现,利用它可以做更多的2D绘制,各组件实际上利用Graphics2D开展各种draw。使用Graphics2D的方式是面向oo的,相当直白。
Graphics2D的使用过程即是根据需要预先调整几个成员对象(策略模式),Stroke/Font/Transformation /RendingHint /Clip/Paint/CompositeRule,然后调用fill/draw/drawstring/drawImage等方法时,即可将指定的几何图形,字符,图像按照预调整的策略,生成像素绘制在屏面上。如:
paint(Graphics g){
Graphics2D g2d = (Graphics2D) g;
g2d.setPaint(new Color(50,50,50,100));
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
g2d.setComposite(AlphaComposite.SrcOver);
g2d.setFont(new Font("romon",0,40));
g2d.drawString("abc", 20, 20);
但是,在swing里从顶层容器得到Graphics并传递给该组件进行绘制之前,事情变得有点复杂,主要还是因为双缓冲的故事,现在继续上一节展开分析。
g = safelyGetGraphics(paintingComponent, c);//通过顶层容器c得到paintingComponent的graph环境。
try {
if (hasBuffer) {//如果使用双缓冲,交给RepaintManager对进行绘制,要求考虑从bufferedComponent开始缓冲支持。
RepaintManager rm = RepaintManager.currentManager(
bufferedComponent);
rm.beginPaint();
try {
rm.paint(paintingComponent, bufferedComponent, g,
paintImmediatelyClip.x,
paintImmediatelyClip.y,
paintImmediatelyClip.width,
paintImmediatelyClip.height);
} finally {
rm.endPaint();
}
}
else {//否则直接对paintingComponent绘制。
g.setClip(paintImmediatelyClip.x,paintImmediatelyClip.y,
paintImmediatelyClip.width,paintImmediatelyClip.height);
paintingComponent.paint(g);
}
} finally {
g.dispose();
}
按java的想法,在这里首先通过顶层容器获得Graphics,之后就可以传给Component来使用,内部将把Graphics的各类调用转换为windows底层绘制api(GDI)实现绘制,最后处理掉Graphics(底层释放资源)。首先分析没有双缓冲的情况。
static Graphics safelyGetGraphics(Component c, Component root) {
synchronized(componentObtainingGraphicsFromLock) {
componentObtainingGraphicsFrom = root;
Graphics g = c.getGraphics();
componentObtainingGraphicsFrom = null;
return g;
}
}
Class Component{
public Graphics getGraphics() {
if (peer instanceof LightweightPeer) {
// This is for a lightweight component, need to
// translate coordinate spaces and clip relative
// to the parent.
if (parent == null) return null;
Graphics g = parent.getGraphics();
if (g == null) return null;
if (g instanceof ConstrainableGraphics) {
((ConstrainableGraphics) g).constrain(x, y, width, height);
} else {
g.translate(x,y);//坐标系平移
g.setClip(0, 0, width, height);//裁剪
}
g.setFont(getFont());//字体
return g;
} else {
ComponentPeer peer = this.peer;
return (peer != null) ? peer.getGraphics() : null;
}
}}
Class WComponentPeer{
public Graphics getGraphics() {
SurfaceData surfaceData = this.surfaceData;
if (!isDisposed() && surfaceData != null) {
/* Fix for bug 4746122. Color and Font shouldn't be null */
Color bgColor = background;//顶层容器背景色或配置
if (bgColor == null) {
bgColor = SystemColor.window;
}
Color fgColor = foreground;
if (fgColor == null) {//顶层容器前景色或配置
fgColor = SystemColor.windowText;
}
Font font = this.font;//顶层容器字体或配置
if (font == null) {
font = defaultFont;
}
ScreenUpdateManager mgr =
ScreenUpdateManager.getInstance();
return mgr.createGraphics(surfaceData, this, fgColor,
bgColor, font);
}
return null;
}
}
因为Graphics对应的是顶层容器的窗口的屏面,所以需要根据该组件的祖宗树不断地递归translate,setClip,即调整坐标系和圈定范围。并且对字体,背景色和前景色都会在此过程中记录默认的设置。当该safelyGetGraphics返回时,再一次根据脏区进行setClip,这样传入组件的Graphics即已经基本调配好绘制策略,可以用来绘制了。
public void paint(Graphics g) {
boolean shouldClearPaintFlags = false;
if ((getWidth() <= 0) || (getHeight() <= 0)) {//检查此时的大小范围
return;
}
Graphics componentGraphics = getComponentGraphics(g);//对g设置本组件的字体和前景色,如果没有指定使用祖宗的设置。
Graphics co = componentGraphics.create();//这里将克隆一个Graphics,克隆的目的是某个函数体内对Graphics的内置对象的预置策略不会影响到其他。
try {
RepaintManager repaintManager = RepaintManager.currentManager(this);
Rectangle clipRect = co.getClipBounds();
int clipX;
int clipY;
int clipW;
int clipH;
if (clipRect == null) {
clipX = clipY = 0;
clipW = getWidth();
clipH = getHeight();
}
else {
clipX = clipRect.x;
clipY = clipRect.y;
clipW = clipRect.width;
clipH = clipRect.height;
}
if(clipW > getWidth()) {//再次调整绘制范围
clipW = getWidth();
}
if(clipH > getHeight()) {
clipH = getHeight();
}
if(getParent() != null && !(getParent() instanceof JComponent)) {
adjustPaintFlags();
shouldClearPaintFlags = true;
}
int bw,bh;
boolean printing = getFlag(IS_PRINTING);
if(!printing && repaintManager.isDoubleBufferingEnabled() &&
!getFlag(ANCESTOR_USING_BUFFER) && isDoubleBuffered()) {//双缓冲处理
repaintManager.beginPaint();
try {
repaintManager.paint(this, this, co, clipX, clipY, clipW,
clipH);
} finally {
repaintManager.endPaint();
}
}
else {//实施绘制
// Will ocassionaly happen in 1.2, especially when printing.
if (clipRect == null) {
co.setClip(clipX, clipY, clipW, clipH);
}
if (!rectangleIsObscured(clipX,clipY,clipW,clipH)) {//判断这块区域是否被opaque子组件完全遮挡,如果是则不需要绘制自身,直接绘制涉及的子组件,这里只检查直系子孙。
if (!printing) {//如果当前不是打印状态,执行绘制。
paintComponent(co);
paintBorder(co);//这里插一句,border是画在整个组件画布范围里面的一圈;当对组件getInsets()时得到的就是border的属性。
}
else {
printComponent(co);
printBorder(co);
}
}
if (!printing) {
paintChildren(co);
}
else {
printChildren(co);
}
}
} finally {
co.dispose();//释放克隆体—这代表了使用Graphics的一个模式,在函数体内将参数graphics先克隆出来,然后使用,finally释放掉克隆体,这样退出函数后不会影响到原Graphics的使用。
if(shouldClearPaintFlags) {
setFlag(ANCESTOR_USING_BUFFER,false);
setFlag(IS_PAINTING_TILE,false);
setFlag(IS_PRINTING,false);
setFlag(IS_PRINTING_ALL,false);
}
}
}
protected void paintComponent(Graphics g) {
if (ui != null) {
Graphics scratchGraphics = (g == null) ? null : g.create();//克隆手法
try {
ui.update(scratchGraphics, this);//委托给ui进行具体绘制。
}
finally {
scratchGraphics.dispose();
}
}
}
protected void paintBorder(Graphics g) {
Border border = getBorder();//没有克隆手法,所以如果需要改变g,则需要自己在paintBorder里克隆。
if (border != null) {
border.paintBorder(this, g, 0, 0, getWidth(), getHeight());
}
}
protected void paintChildren(Graphics g) {
boolean isJComponent;
Graphics sg = g;
synchronized(getTreeLock()) {
int i = getComponentCount() - 1;//如果没有子组件直接返回
if (i < 0) {
return;
}
// If we are only to paint to a specific child, determine
// its index.
if (paintingChild != null &&
(paintingChild instanceof JComponent) &&
((JComponent)paintingChild).isOpaque()) {//如果指定了重画子组件(见纪要二),则意味着从该子组件开始往前都可能绘制
for (; i >= 0; i--) {
if (getComponent(i) == paintingChild){
break;
}
}
}
Rectangle tmpRect = fetchRectangle();
boolean checkSiblings = (!isOptimizedDrawingEnabled() &&
checkIfChildObscuredBySibling());//如果不能保证子组件不会相互遮挡并且要求检查
Rectangle clipBounds = null;
if (checkSiblings) {
clipBounds = sg.getClipBounds();//得到剪裁区
if (clipBounds == null) {
clipBounds = new Rectangle(0, 0, getWidth(),
getHeight());
}
}
boolean printing = getFlag(IS_PRINTING);
for (; i >= 0 ; i--) {//对每个子组件进行遍历
Component comp = getComponent(i);
isJComponent = (comp instanceof JComponent);
if (comp != null &&
(isJComponent || isLightweightComponent(comp)) &&
(comp.isVisible() == true)) {//如果是轻量级组件且visible
Rectangle cr;
cr = comp.getBounds(tmpRect);
boolean hitClip = g.hitClip(cr.x, cr.y, cr.width,
cr.height);//检查该子组件是否和要绘制的区域相交
if (hitClip) {//如果相交需要绘制
if (checkSiblings && i > 0) {//如果不是最后一个子组件且要求检查相互遮挡的情况
int x = cr.x;
int y = cr.y;
int width = cr.width;
int height = cr.height;
SwingUtilities.computeIntersection
(clipBounds.x, clipBounds.y,
clipBounds.width, clipBounds.height, cr);
if(getObscuredState(i, cr.x, cr.y, cr.width,
cr.height) == COMPLETELY_OBSCURED) {//检查发现相交区被其他组件遮挡则不必绘制该子组件
continue;
}
cr.x = x;
cr.y = y;
cr.width = width;
cr.height = height;
}
Graphics cg = sg.create(cr.x, cr.y, cr.width,
cr.height);//克隆手法
cg.setColor(comp.getForeground());//设置前景色
cg.setFont(comp.getFont());//设置字体
boolean shouldSetFlagBack = false;//下文调整标志位后即开始绘制。
try {
if(isJComponent) {
if(getFlag(ANCESTOR_USING_BUFFER)) {
((JComponent)comp).setFlag(
ANCESTOR_USING_BUFFER,true);
shouldSetFlagBack = true;
}
if(getFlag(IS_PAINTING_TILE)) {
((JComponent)comp).setFlag(
IS_PAINTING_TILE,true);
shouldSetFlagBack = true;
}
if(!printing) {
((JComponent)comp).paint(cg);
}
else {
if (!getFlag(IS_PRINTING_ALL)) {
comp.print(cg);//
}
else {
comp.printAll(cg);
}
}
} else {
if (!printing) {
comp.paint(cg);
}
else {
if (!getFlag(IS_PRINTING_ALL)) {
comp.print(cg);
}
else {
comp.printAll(cg);
}
}
}
} finally {
cg.dispose();
if(shouldSetFlagBack) {
((JComponent)comp).setFlag(
ANCESTOR_USING_BUFFER,false);
((JComponent)comp).setFlag(
IS_PAINTING_TILE,false);
}
}
}
}
}
recycleRectangle(tmpRect);
}
}
以上的绘制长话短说,是因为swing的容器概念,使得我们对某一个组件的绘制总是要包含其子组件的绘制,所以需要根据待绘制区和子组件的情况不断进行筛选绘制。毫无疑问,这种遍历也是一个比较大的开销,尤其是组件层次比较多的情况下。在具体绘制时swing委托给ui来进行具体的java2d绘制,以上的分析graphics2d不管是不是克隆的手法,都是代表实际屏面的,即没有使用双缓冲机制,下文分析双缓冲的情况。
双缓冲将分为两种情况,一种是每窗口双缓冲的策略,一种直接双缓冲。每窗口双缓冲时RepaintManager将使用BufferStrategyPaintManager,此时beginPaint将达到一个同步效果,即如果在awt-windows中正在底层show,则此时的paint将等待。
BufferStrategyPaintManager{
public void beginPaint() {
synchronized(this) {
painting = true;
// Make sure another thread isn't attempting to show from
// the back buffer.
while(showing) {
try {
wait();
} catch (InterruptedException ie) {
}
}
}
这也是beginPaint/endPaint的主要目的。对于非每窗口双缓冲,beginPaint/endPaint组合对是没有用处的。
先来看普通的双缓冲:
Class RepaintManager{
void paint(JComponent paintingComponent,
JComponent bufferComponent, Graphics g,
int x, int y, int w, int h) {
PaintManager paintManager = getPaintManager();
if (!isPaintingThread()) {
// We're painting to two threads at once. PaintManager deals
// with this a bit better than BufferStrategyPaintManager, use
// it to avoid possible exceptions/corruption.
if (paintManager.getClass() != PaintManager.class) {
paintManager = new PaintManager();
paintManager.repaintManager = this;
}
}
if (!paintManager.paint(paintingComponent, bufferComponent, g,
x, y, w, h)) {//委托给合适的PaintManager进行绘制
g.setClip(x, y, w, h);//如果绘制失败,则将直接绘制到屏面上。
paintingComponent.paintToOffscreen(g, x, y, w, h, x + w, y + h);//就像注释里说的,这个方法只在这种极端情况下被调用,其执行逻辑和非缓冲的类似但更粗糙些。
}
}
/**
* Paints to the specified graphics. This does not set the clip and it
* does not adjust the Graphics in anyway, callers must do that first.
* This method is package-private for RepaintManager.PaintManager and
* its subclasses to call, it is NOT intended for general use outside
* of that.
*/
void paintToOffscreen(Graphics g, int x, int y, int w, int h, int maxX,
int maxY) {
try {
setFlag(ANCESTOR_USING_BUFFER, true);
if ((y + h) < maxY || (x + w) < maxX) {
setFlag(IS_PAINTING_TILE, true);
}
if (getFlag(IS_REPAINTING)) {
// Called from paintImmediately (RepaintManager) to fill
// repaint request
paint(g);
} else {
// Called from paint() (AWT) to repair damage
if(!rectangleIsObscured(x, y, w, h)) {
paintComponent(g);
paintBorder(g);
}
paintChildren(g);
}
} finally {
setFlag(ANCESTOR_USING_BUFFER, false);
setFlag(IS_PAINTING_TILE, false);
}
}
Class PaintManager{
public boolean paint(JComponent paintingComponent,
JComponent bufferComponent, Graphics g,
int x, int y, int w, int h) {
// First attempt to use VolatileImage buffer for performance.
// If this fails (which should rarely occur), fallback to a
// standard Image buffer.
//Graphics总是尽力先获得一个VolatileImage,因为该Image得到了性能优化,比如它可能直接会在显示内存区分配有对//应空间,但是正因为此,该Image是受系统资源限制而易变的,因此使用起来需要验证有效性。
boolean paintCompleted = false;
Image offscreen;
if (repaintManager.useVolatileDoubleBuffer() &&
(offscreen = getValidImage(repaintManager.
getVolatileOffscreenBuffer(bufferComponent, w, h))) != null) {
VolatileImage vImage = (java.awt.image.VolatileImage)offscreen;
GraphicsConfiguration gc = bufferComponent.
getGraphicsConfiguration();
for (int i = 0; !paintCompleted &&
i < RepaintManager.VOLATILE_LOOP_MAX; i++) {//在此参数范围内swing总是试图用VolatileImage完成绘制
if (vImage.validate(gc) ==
VolatileImage.IMAGE_INCOMPATIBLE) {//验证vImage的可用性
repaintManager.resetVolatileDoubleBuffer(gc);
offscreen = repaintManager.getVolatileOffscreenBuffer(
bufferComponent,w, h);
vImage = (java.awt.image.VolatileImage)offscreen;
}
paintDoubleBuffered(paintingComponent, vImage, g, x, y,
w, h);//具体的绘制
paintCompleted = !vImage.contentsLost();//检查自上次验证后的绘制是否生效
}
}
// VolatileImage painting loop failed, fallback to regular
// offscreen buffer
if (!paintCompleted && (offscreen = getValidImage(
repaintManager.getOffscreenBuffer(
bufferComponent, w, h))) != null) {//创建普通Image
paintDoubleBuffered(paintingComponent, offscreen, g, x, y, w,
h);
paintCompleted = true;
}
return paintCompleted;
}
protected void paintDoubleBuffered(JComponent c, Image image,
Graphics g, int clipX, int clipY,
int clipW, int clipH) {
Graphics osg = image.getGraphics();//从image中获得Graphics对象
int bw = Math.min(clipW, image.getWidth(null));
int bh = Math.min(clipH, image.getHeight(null));
int x,y,maxx,maxy;
try {
for(x = clipX, maxx = clipX+clipW; x < maxx ; x += bw ) {
for(y=clipY, maxy = clipY + clipH; y < maxy ; y += bh) {
osg.translate(-x, -y);
osg.setClip(x,y,bw,bh);
c.paintToOffscreen(osg, x, y, bw, bh, maxx, maxy);//以该image为屏面进行绘制
g.setClip(x, y, bw, bh);
g.drawImage(image, x, y, c);//将image绘制到真正的屏面上。
osg.translate(x, y);
}
}
} finally {
osg.dispose();
}
}
双缓冲下的实现长话短说就是利用java2d中对Image的处理支持。通过在内存中构造一个Image,这就代表了一个可绘制的屏面,可以此为屏面从而获得Graphics,而后让组件的绘制使用此Graphics对象。当组件完成绘制后,仅仅是改变了该Image。最后由swing将Image一次性画到真正的窗口屏面上去。为了提高性能,java2d提供了volatileImage,并且各Image的创建时都从相应的GraphicsConfiguration创建以保证colormodal等兼容性从而可以快速将Image绘制到真正的屏面上。
对每窗口双缓冲的策略,绘制则是要针对每个窗口对应的缓冲Image进行。
前两节实现了Swing的绘制机制,即在整个GUI应用中所有的绘制时机是怎么产生的,时机产生后又怎么样经过swing框架的基础处理最终进入合适组件开展具体paint。那么每个组件在得到绘制时机时,如何进行绘制?这就从计算机显示世界的历史开始说起了。
在很久很久以前老一辈革命家是通过INT10直接向显示区内存书写显示字节数据,显示芯片会将这些数据按频率生成模拟信号提供显示器进行显示。此后发生了很多方面的进化,一方面是语言上有了c,c++,所以我们不再直接int10,而是调用其API函数;另一方面是绘制的各个方面被封装化,不再直接画点像素,而是逐渐抽象封装了很多绘制工具包,即如果要绘制一个立方体,将有函数调用,提供其顶点位置即可;再一方面则是硬件开发参与进来,为了提高性能,很多软件上的实现,比如当调整角度时一个立方体的显示将发生像素的变化,这个计算被转移到硬件上实现。这些方面的进化是同步展开的:首先说windows为了隔离不同主机的各类厂商不同的显卡其不同的指令约定,提供了GDI统一API供应用程序调用,windows底层将利用各类厂商的驱动来完成映射,此后windows在.net框架上以GDI+的形式包装了GDI以提供OO的调用形式;于此同时,面对那些对绘制性能要求非常高的应用程序,尤其是3D游戏,为了能够让开发的程序更加迅速地绘制,windows提供了Direct API工具包,其调用绕过了许多环节,迅速操作显卡的显示内存区来完成绘制。Direct因其(for speed)使命决定了此后的由软件设计商来主导硬件开发商设计的历史。随着3D绘制需求日益膨胀,direct不断升级版本,提供了很多更高级的调用函数封装来简化开发,这不仅仅是可调用函数的数量增长,而且是提出了适应开发需求的新的绘制体系。只有全面支持direct的硬件厂商才有更好的出路,于是他们纷纷追寻direct的基调进行硬件设计以实现direct显示加速。到了现代,在windows体系里,如果要开发一般的GUI,则GDI/GDI+是合适的选择,虽然GDI只有一部分可能借助硬件加速,GDI+则完全没有硬件加速,但是应付一般的GUI绰绰有余,关键这是普适任何环境的;如果要开发3D游戏或其他图形应用,则direct是合适的选择,虽然如果主机没有direct显卡将可能无法运行,但是现在的显卡绝大多数都支持direct;而最近microsoft在.net3推出的WPF,则是利用direct3d封装及xml描述来提供统一绘制API。在之前提到的windows启动桌面窗口管理器(Desktop Window Manager,DWM)技术后java2d将默认不使用双缓冲,因为DWM自身实现了窗口双缓冲机制,这个dwm就是建立在WPF基础之上。
Java的想法是在各操作系统上封装其底层图形API提供一个统一的图形绘制工具包,不管是2d还是3d,最终利用java绘制时将把调用转换为操作系统的底层图形进行调用。在Windows平台下要进行绘制,首先要打开一个窗口,而后的底层绘制api调用都要跟这个窗口相关(以窗口作为调用的一个参数或者内置为一个成员变量,从而可以在操作系统层面控制各个应用程序的统一表现,比如防止某个应用程序去绘制另外一个应用程序的窗口),这个设计原则适用于各类窗口操作系统。那么在java swing应用程序中,当打开顶层容器时,一个wwindowpeer窗口对等体被构造,这就对应了底层的一个window窗口,在peer构造方法中将会对此peer作为构造参数包出一个顶层容器的SurfaceData屏面成员对象。此后当绘制时机到来,经框架层层过滤确定要进行本容器内任何一个组件的paint时,框架会追溯组件的祖宗树,最终回到该顶层容器里safelyGetGraphics,这里将会构造一个new Graphics对象,同时将此顶层容器的SurfaceData屏面作为该Graphics对象的构造参数。经过以上的设计,组件的paint中使用safelyGetGraphics过来的Graphics开展的各种draw,都可在具体落实到屏幕时找到对应的底层窗口从而有效地转换成底层windows绘制api完成绘制。
Graphics2D是Graphics的加强版实现,利用它可以做更多的2D绘制,各组件实际上利用Graphics2D开展各种draw。使用Graphics2D的方式是面向oo的,相当直白。
Graphics2D的使用过程即是根据需要预先调整几个成员对象(策略模式),Stroke/Font/Transformation /RendingHint /Clip/Paint/CompositeRule,然后调用fill/draw/drawstring/drawImage等方法时,即可将指定的几何图形,字符,图像按照预调整的策略,生成像素绘制在屏面上。如:
paint(Graphics g){
Graphics2D g2d = (Graphics2D) g;
g2d.setPaint(new Color(50,50,50,100));
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
g2d.setComposite(AlphaComposite.SrcOver);
g2d.setFont(new Font("romon",0,40));
g2d.drawString("abc", 20, 20);
但是,在swing里从顶层容器得到Graphics并传递给该组件进行绘制之前,事情变得有点复杂,主要还是因为双缓冲的故事,现在继续上一节展开分析。
g = safelyGetGraphics(paintingComponent, c);//通过顶层容器c得到paintingComponent的graph环境。
try {
if (hasBuffer) {//如果使用双缓冲,交给RepaintManager对进行绘制,要求考虑从bufferedComponent开始缓冲支持。
RepaintManager rm = RepaintManager.currentManager(
bufferedComponent);
rm.beginPaint();
try {
rm.paint(paintingComponent, bufferedComponent, g,
paintImmediatelyClip.x,
paintImmediatelyClip.y,
paintImmediatelyClip.width,
paintImmediatelyClip.height);
} finally {
rm.endPaint();
}
}
else {//否则直接对paintingComponent绘制。
g.setClip(paintImmediatelyClip.x,paintImmediatelyClip.y,
paintImmediatelyClip.width,paintImmediatelyClip.height);
paintingComponent.paint(g);
}
} finally {
g.dispose();
}
按java的想法,在这里首先通过顶层容器获得Graphics,之后就可以传给Component来使用,内部将把Graphics的各类调用转换为windows底层绘制api(GDI)实现绘制,最后处理掉Graphics(底层释放资源)。首先分析没有双缓冲的情况。
static Graphics safelyGetGraphics(Component c, Component root) {
synchronized(componentObtainingGraphicsFromLock) {
componentObtainingGraphicsFrom = root;
Graphics g = c.getGraphics();
componentObtainingGraphicsFrom = null;
return g;
}
}
Class Component{
public Graphics getGraphics() {
if (peer instanceof LightweightPeer) {
// This is for a lightweight component, need to
// translate coordinate spaces and clip relative
// to the parent.
if (parent == null) return null;
Graphics g = parent.getGraphics();
if (g == null) return null;
if (g instanceof ConstrainableGraphics) {
((ConstrainableGraphics) g).constrain(x, y, width, height);
} else {
g.translate(x,y);//坐标系平移
g.setClip(0, 0, width, height);//裁剪
}
g.setFont(getFont());//字体
return g;
} else {
ComponentPeer peer = this.peer;
return (peer != null) ? peer.getGraphics() : null;
}
}}
Class WComponentPeer{
public Graphics getGraphics() {
SurfaceData surfaceData = this.surfaceData;
if (!isDisposed() && surfaceData != null) {
/* Fix for bug 4746122. Color and Font shouldn't be null */
Color bgColor = background;//顶层容器背景色或配置
if (bgColor == null) {
bgColor = SystemColor.window;
}
Color fgColor = foreground;
if (fgColor == null) {//顶层容器前景色或配置
fgColor = SystemColor.windowText;
}
Font font = this.font;//顶层容器字体或配置
if (font == null) {
font = defaultFont;
}
ScreenUpdateManager mgr =
ScreenUpdateManager.getInstance();
return mgr.createGraphics(surfaceData, this, fgColor,
bgColor, font);
}
return null;
}
}
因为Graphics对应的是顶层容器的窗口的屏面,所以需要根据该组件的祖宗树不断地递归translate,setClip,即调整坐标系和圈定范围。并且对字体,背景色和前景色都会在此过程中记录默认的设置。当该safelyGetGraphics返回时,再一次根据脏区进行setClip,这样传入组件的Graphics即已经基本调配好绘制策略,可以用来绘制了。
public void paint(Graphics g) {
boolean shouldClearPaintFlags = false;
if ((getWidth() <= 0) || (getHeight() <= 0)) {//检查此时的大小范围
return;
}
Graphics componentGraphics = getComponentGraphics(g);//对g设置本组件的字体和前景色,如果没有指定使用祖宗的设置。
Graphics co = componentGraphics.create();//这里将克隆一个Graphics,克隆的目的是某个函数体内对Graphics的内置对象的预置策略不会影响到其他。
try {
RepaintManager repaintManager = RepaintManager.currentManager(this);
Rectangle clipRect = co.getClipBounds();
int clipX;
int clipY;
int clipW;
int clipH;
if (clipRect == null) {
clipX = clipY = 0;
clipW = getWidth();
clipH = getHeight();
}
else {
clipX = clipRect.x;
clipY = clipRect.y;
clipW = clipRect.width;
clipH = clipRect.height;
}
if(clipW > getWidth()) {//再次调整绘制范围
clipW = getWidth();
}
if(clipH > getHeight()) {
clipH = getHeight();
}
if(getParent() != null && !(getParent() instanceof JComponent)) {
adjustPaintFlags();
shouldClearPaintFlags = true;
}
int bw,bh;
boolean printing = getFlag(IS_PRINTING);
if(!printing && repaintManager.isDoubleBufferingEnabled() &&
!getFlag(ANCESTOR_USING_BUFFER) && isDoubleBuffered()) {//双缓冲处理
repaintManager.beginPaint();
try {
repaintManager.paint(this, this, co, clipX, clipY, clipW,
clipH);
} finally {
repaintManager.endPaint();
}
}
else {//实施绘制
// Will ocassionaly happen in 1.2, especially when printing.
if (clipRect == null) {
co.setClip(clipX, clipY, clipW, clipH);
}
if (!rectangleIsObscured(clipX,clipY,clipW,clipH)) {//判断这块区域是否被opaque子组件完全遮挡,如果是则不需要绘制自身,直接绘制涉及的子组件,这里只检查直系子孙。
if (!printing) {//如果当前不是打印状态,执行绘制。
paintComponent(co);
paintBorder(co);//这里插一句,border是画在整个组件画布范围里面的一圈;当对组件getInsets()时得到的就是border的属性。
}
else {
printComponent(co);
printBorder(co);
}
}
if (!printing) {
paintChildren(co);
}
else {
printChildren(co);
}
}
} finally {
co.dispose();//释放克隆体—这代表了使用Graphics的一个模式,在函数体内将参数graphics先克隆出来,然后使用,finally释放掉克隆体,这样退出函数后不会影响到原Graphics的使用。
if(shouldClearPaintFlags) {
setFlag(ANCESTOR_USING_BUFFER,false);
setFlag(IS_PAINTING_TILE,false);
setFlag(IS_PRINTING,false);
setFlag(IS_PRINTING_ALL,false);
}
}
}
protected void paintComponent(Graphics g) {
if (ui != null) {
Graphics scratchGraphics = (g == null) ? null : g.create();//克隆手法
try {
ui.update(scratchGraphics, this);//委托给ui进行具体绘制。
}
finally {
scratchGraphics.dispose();
}
}
}
protected void paintBorder(Graphics g) {
Border border = getBorder();//没有克隆手法,所以如果需要改变g,则需要自己在paintBorder里克隆。
if (border != null) {
border.paintBorder(this, g, 0, 0, getWidth(), getHeight());
}
}
protected void paintChildren(Graphics g) {
boolean isJComponent;
Graphics sg = g;
synchronized(getTreeLock()) {
int i = getComponentCount() - 1;//如果没有子组件直接返回
if (i < 0) {
return;
}
// If we are only to paint to a specific child, determine
// its index.
if (paintingChild != null &&
(paintingChild instanceof JComponent) &&
((JComponent)paintingChild).isOpaque()) {//如果指定了重画子组件(见纪要二),则意味着从该子组件开始往前都可能绘制
for (; i >= 0; i--) {
if (getComponent(i) == paintingChild){
break;
}
}
}
Rectangle tmpRect = fetchRectangle();
boolean checkSiblings = (!isOptimizedDrawingEnabled() &&
checkIfChildObscuredBySibling());//如果不能保证子组件不会相互遮挡并且要求检查
Rectangle clipBounds = null;
if (checkSiblings) {
clipBounds = sg.getClipBounds();//得到剪裁区
if (clipBounds == null) {
clipBounds = new Rectangle(0, 0, getWidth(),
getHeight());
}
}
boolean printing = getFlag(IS_PRINTING);
for (; i >= 0 ; i--) {//对每个子组件进行遍历
Component comp = getComponent(i);
isJComponent = (comp instanceof JComponent);
if (comp != null &&
(isJComponent || isLightweightComponent(comp)) &&
(comp.isVisible() == true)) {//如果是轻量级组件且visible
Rectangle cr;
cr = comp.getBounds(tmpRect);
boolean hitClip = g.hitClip(cr.x, cr.y, cr.width,
cr.height);//检查该子组件是否和要绘制的区域相交
if (hitClip) {//如果相交需要绘制
if (checkSiblings && i > 0) {//如果不是最后一个子组件且要求检查相互遮挡的情况
int x = cr.x;
int y = cr.y;
int width = cr.width;
int height = cr.height;
SwingUtilities.computeIntersection
(clipBounds.x, clipBounds.y,
clipBounds.width, clipBounds.height, cr);
if(getObscuredState(i, cr.x, cr.y, cr.width,
cr.height) == COMPLETELY_OBSCURED) {//检查发现相交区被其他组件遮挡则不必绘制该子组件
continue;
}
cr.x = x;
cr.y = y;
cr.width = width;
cr.height = height;
}
Graphics cg = sg.create(cr.x, cr.y, cr.width,
cr.height);//克隆手法
cg.setColor(comp.getForeground());//设置前景色
cg.setFont(comp.getFont());//设置字体
boolean shouldSetFlagBack = false;//下文调整标志位后即开始绘制。
try {
if(isJComponent) {
if(getFlag(ANCESTOR_USING_BUFFER)) {
((JComponent)comp).setFlag(
ANCESTOR_USING_BUFFER,true);
shouldSetFlagBack = true;
}
if(getFlag(IS_PAINTING_TILE)) {
((JComponent)comp).setFlag(
IS_PAINTING_TILE,true);
shouldSetFlagBack = true;
}
if(!printing) {
((JComponent)comp).paint(cg);
}
else {
if (!getFlag(IS_PRINTING_ALL)) {
comp.print(cg);//
}
else {
comp.printAll(cg);
}
}
} else {
if (!printing) {
comp.paint(cg);
}
else {
if (!getFlag(IS_PRINTING_ALL)) {
comp.print(cg);
}
else {
comp.printAll(cg);
}
}
}
} finally {
cg.dispose();
if(shouldSetFlagBack) {
((JComponent)comp).setFlag(
ANCESTOR_USING_BUFFER,false);
((JComponent)comp).setFlag(
IS_PAINTING_TILE,false);
}
}
}
}
}
recycleRectangle(tmpRect);
}
}
以上的绘制长话短说,是因为swing的容器概念,使得我们对某一个组件的绘制总是要包含其子组件的绘制,所以需要根据待绘制区和子组件的情况不断进行筛选绘制。毫无疑问,这种遍历也是一个比较大的开销,尤其是组件层次比较多的情况下。在具体绘制时swing委托给ui来进行具体的java2d绘制,以上的分析graphics2d不管是不是克隆的手法,都是代表实际屏面的,即没有使用双缓冲机制,下文分析双缓冲的情况。
双缓冲将分为两种情况,一种是每窗口双缓冲的策略,一种直接双缓冲。每窗口双缓冲时RepaintManager将使用BufferStrategyPaintManager,此时beginPaint将达到一个同步效果,即如果在awt-windows中正在底层show,则此时的paint将等待。
BufferStrategyPaintManager{
public void beginPaint() {
synchronized(this) {
painting = true;
// Make sure another thread isn't attempting to show from
// the back buffer.
while(showing) {
try {
wait();
} catch (InterruptedException ie) {
}
}
}
这也是beginPaint/endPaint的主要目的。对于非每窗口双缓冲,beginPaint/endPaint组合对是没有用处的。
先来看普通的双缓冲:
Class RepaintManager{
void paint(JComponent paintingComponent,
JComponent bufferComponent, Graphics g,
int x, int y, int w, int h) {
PaintManager paintManager = getPaintManager();
if (!isPaintingThread()) {
// We're painting to two threads at once. PaintManager deals
// with this a bit better than BufferStrategyPaintManager, use
// it to avoid possible exceptions/corruption.
if (paintManager.getClass() != PaintManager.class) {
paintManager = new PaintManager();
paintManager.repaintManager = this;
}
}
if (!paintManager.paint(paintingComponent, bufferComponent, g,
x, y, w, h)) {//委托给合适的PaintManager进行绘制
g.setClip(x, y, w, h);//如果绘制失败,则将直接绘制到屏面上。
paintingComponent.paintToOffscreen(g, x, y, w, h, x + w, y + h);//就像注释里说的,这个方法只在这种极端情况下被调用,其执行逻辑和非缓冲的类似但更粗糙些。
}
}
/**
* Paints to the specified graphics. This does not set the clip and it
* does not adjust the Graphics in anyway, callers must do that first.
* This method is package-private for RepaintManager.PaintManager and
* its subclasses to call, it is NOT intended for general use outside
* of that.
*/
void paintToOffscreen(Graphics g, int x, int y, int w, int h, int maxX,
int maxY) {
try {
setFlag(ANCESTOR_USING_BUFFER, true);
if ((y + h) < maxY || (x + w) < maxX) {
setFlag(IS_PAINTING_TILE, true);
}
if (getFlag(IS_REPAINTING)) {
// Called from paintImmediately (RepaintManager) to fill
// repaint request
paint(g);
} else {
// Called from paint() (AWT) to repair damage
if(!rectangleIsObscured(x, y, w, h)) {
paintComponent(g);
paintBorder(g);
}
paintChildren(g);
}
} finally {
setFlag(ANCESTOR_USING_BUFFER, false);
setFlag(IS_PAINTING_TILE, false);
}
}
Class PaintManager{
public boolean paint(JComponent paintingComponent,
JComponent bufferComponent, Graphics g,
int x, int y, int w, int h) {
// First attempt to use VolatileImage buffer for performance.
// If this fails (which should rarely occur), fallback to a
// standard Image buffer.
//Graphics总是尽力先获得一个VolatileImage,因为该Image得到了性能优化,比如它可能直接会在显示内存区分配有对//应空间,但是正因为此,该Image是受系统资源限制而易变的,因此使用起来需要验证有效性。
boolean paintCompleted = false;
Image offscreen;
if (repaintManager.useVolatileDoubleBuffer() &&
(offscreen = getValidImage(repaintManager.
getVolatileOffscreenBuffer(bufferComponent, w, h))) != null) {
VolatileImage vImage = (java.awt.image.VolatileImage)offscreen;
GraphicsConfiguration gc = bufferComponent.
getGraphicsConfiguration();
for (int i = 0; !paintCompleted &&
i < RepaintManager.VOLATILE_LOOP_MAX; i++) {//在此参数范围内swing总是试图用VolatileImage完成绘制
if (vImage.validate(gc) ==
VolatileImage.IMAGE_INCOMPATIBLE) {//验证vImage的可用性
repaintManager.resetVolatileDoubleBuffer(gc);
offscreen = repaintManager.getVolatileOffscreenBuffer(
bufferComponent,w, h);
vImage = (java.awt.image.VolatileImage)offscreen;
}
paintDoubleBuffered(paintingComponent, vImage, g, x, y,
w, h);//具体的绘制
paintCompleted = !vImage.contentsLost();//检查自上次验证后的绘制是否生效
}
}
// VolatileImage painting loop failed, fallback to regular
// offscreen buffer
if (!paintCompleted && (offscreen = getValidImage(
repaintManager.getOffscreenBuffer(
bufferComponent, w, h))) != null) {//创建普通Image
paintDoubleBuffered(paintingComponent, offscreen, g, x, y, w,
h);
paintCompleted = true;
}
return paintCompleted;
}
protected void paintDoubleBuffered(JComponent c, Image image,
Graphics g, int clipX, int clipY,
int clipW, int clipH) {
Graphics osg = image.getGraphics();//从image中获得Graphics对象
int bw = Math.min(clipW, image.getWidth(null));
int bh = Math.min(clipH, image.getHeight(null));
int x,y,maxx,maxy;
try {
for(x = clipX, maxx = clipX+clipW; x < maxx ; x += bw ) {
for(y=clipY, maxy = clipY + clipH; y < maxy ; y += bh) {
osg.translate(-x, -y);
osg.setClip(x,y,bw,bh);
c.paintToOffscreen(osg, x, y, bw, bh, maxx, maxy);//以该image为屏面进行绘制
g.setClip(x, y, bw, bh);
g.drawImage(image, x, y, c);//将image绘制到真正的屏面上。
osg.translate(x, y);
}
}
} finally {
osg.dispose();
}
}
双缓冲下的实现长话短说就是利用java2d中对Image的处理支持。通过在内存中构造一个Image,这就代表了一个可绘制的屏面,可以此为屏面从而获得Graphics,而后让组件的绘制使用此Graphics对象。当组件完成绘制后,仅仅是改变了该Image。最后由swing将Image一次性画到真正的窗口屏面上去。为了提高性能,java2d提供了volatileImage,并且各Image的创建时都从相应的GraphicsConfiguration创建以保证colormodal等兼容性从而可以快速将Image绘制到真正的屏面上。
对每窗口双缓冲的策略,绘制则是要针对每个窗口对应的缓冲Image进行。
发表评论
-
JAVA Painting-Swing实现纪要四
2008-12-17 09:01 1801JAVA Painting-Swing实现纪要四 前三节大概描 ... -
JAVA Painting-Swing实现纪要二
2008-12-07 17:46 2202JAVA Painting-Swing实现纪要二 然后继续&l ... -
JAVA Painting-Swing实现纪要一
2008-11-28 15:38 5083首先推荐<Painting in AWT ... -
JAVA InputMethod 输入法实现纪要
2008-11-28 09:46 12658Jre1.7对输入法的支持使得java开发者能够方便地使用JA ... -
JAVA MouseEvent实现纪要
2008-11-28 09:45 1984Jre1.7鼠标事件以MouseEvent类封装。共有如下8种 ... -
Java Focus实现纪要三
2008-11-28 09:44 1163特别注意:按java给出的 ... -
Java Focus实现纪要二
2008-11-28 09:44 16151. 在Jre1.7版本 ... -
Java Focus实现纪要一
2008-11-28 09:43 2720窗口系统一般包含一个 ... -
Java dnd拖拽实现分析纪要
2008-11-28 09:41 2910Java dnd拖拽实现分析纪要 既有的Swing组件都内置 ...
相关推荐
从给出的文件名 "Painting-With-Music-源码.rar" 和 "Painting-With-Music-源码.zip" 来看,这很可能是该项目的源代码文件,包含了实现这一功能的所有编程代码。 在这个项目中,我们可以深入探讨以下几个关键知识点...
在这个名为“涂鸦wx-app-painting-master.zip”的压缩包中,我们可能找到了一个专门用于在微信小程序上实现涂鸦功能的项目。下面将详细探讨微信小程序的基础知识、涂鸦功能的实现以及可能包含的文件结构。 首先,...
This is a painting-tool. You can choose several drawing options from a toolbar and you can apply much filters and effects to the pic. Pls. give feedback!
绘画是一种创造性和想象力的艺术形式,许多人通过绘画表达自己的情感和思想。但传统绘画需要大量时间和精力,对许多人来说是困难的。...用户可以轻松进行各种绘画操作,实现自己的创意和想象,帮助广大绘画爱好者。
《Painting-Pro-Estimator:开源的绘画与装饰预算及发票解决方案》 在当今的商业环境中,对于画家和装饰师来说,精确、高效的估价和发票管理是业务成功的关键因素。"Painting-Pro-Estimator"就是这样一款专为这类...
通过监听音乐的音频数据,项目将音乐的节奏、频率等元素转化为视觉效果,实现了音乐与绘画的实时互动。 在"Painting-With-Music-master"这个压缩包中,包含了项目的所有源代码和资源文件。开发者可能通过分析这些...
中国传统山水画数据集 文章标题:“利用生成的对抗网络进行端到端的中国山水画创作” ArXiv: : 抽象的:当前基于GAN的艺术生成方法由于依赖条件输入而产生非原创的艺术作品。在这里,我们提出GAN(SAPGAN),这是...
总的来说,"wash-painting-ui"项目是将传统艺术与现代技术完美融合的典范,通过Vue3和Vite的技术支持,实现了富有创意的水墨风格UI设计。每个组件都精心设计,力求在用户体验与艺术美感之间找到最佳平衡。这种大胆的...
在这个"painting-project"中,`<canvas>`元素是关键,它提供了一个画布,JavaScript通常会与之配合,通过Canvas API来实现动态绘图。Canvas API允许开发者在网页上进行像素级别的操作,绘制线条、形状、图像,甚至...
"light-painting-wand"项目是一个创新的科技艺术工具,专为光绘摄影爱好者和艺术家设计。光绘,又称光涂鸦,是一种摄影技术,通过在黑暗中移动光源来创造艺术图像。这个项目旨在提供一个名为“轻涂棒”的硬件设备,...
这个"painting-algorithms"项目显然集中于使用编程语言,特别是Python,来实现各种绘画算法。让我们深入探讨一下这个主题。 首先,绘画算法的核心是将几何数据转化为屏幕上的像素颜色。这些算法可以分为基本和复杂...
"Painting-Cube" 是一个基于C#编程语言的项目,可能是一个三维绘画或渲染软件,或者是关于立方体图形处理的教程。从标题来看,它很可能涉及到计算机图形学中的3D建模和绘制技术,特别是在C#环境中实现的。下面我们将...
总之,"java 画图软件_java_" 提供了一个用Java实现的简单图形绘制工具的例子,展示了如何利用Java的GUI库来创建具有图形绘制能力的桌面应用程序。通过深入学习和理解这个项目,开发者可以进一步提升他们的Java GUI...
"dot-painting-challenge"项目就是一个很好的例子,它利用编程语言的力量,借助乌龟图形库和颜色图库,以彩色圆点的形式创作出各种图像。本文将深入探讨这一挑战背后的技术细节。 首先,乌龟图形库(Turtle ...
5. **绘图(painting)**:在"painting"目录中,你将看到如何利用Java的Graphics2D API进行图形绘制。这包括在组件上绘制线条、形状、图像甚至动画,提供了自定义组件外观的强大能力。 通过深入学习这些示例,你...
合并单元格绘画-基于图像的分析管道配方 :...目前,这两种数据成分的测量都是由CellProfiler软件进行的(使用定制的Pooled Cell Painting插件)。 配方步骤 所有食谱还包括针对每个食谱的特定说明或步骤。 我们的食谱
5. **颜色和工具选择**:应用可能提供了丰富的颜色选择器和不同类型的绘画工具,比如铅笔、刷子、橡皮等,这些功能都是通过JavaScript实现的。 6. **保存和分享功能**:用户完成的画作可以被保存在本地或上传到云端...
在这个项目中,开发者运用了JavaScript作为主要的编程语言,这表明网站的交互性和动态功能主要通过JavaScript实现。 React是一个由Facebook维护的开源JavaScript库,专门用于构建用户界面,特别是单页应用程序(SPA...
用机器人进行轻涂 宜居人士: ... 3-Ouvrir le fichiersouhaitéavec Arduino IDE 特殊形式的证明书,普通记者通讯社 基本三驾马车规则(使用说明): PP_SINGLE_IMAGE.ino 内容基本编码moteur pasàpas a
在本项目中,"MFC-painting-(Kaleidoscope).rar"是一个使用Microsoft Foundation Class (MFC)库开发的Visual C++应用,主要目的是实现一个动态的万花筒效果。MFC是微软提供的一套面向对象的C++类库,它封装了Windows...