Java AWT/Swing实现不规则窗体和控件

Oracle裁员,关注了下Oracle,但依旧并不喜欢这家公司,一直觉得它经营不好Java。就好像Microsoft经营不好Linux一样(很奇怪,但总觉得会有这么一天!) 不写Java已经十余年了,但最近还是想起一些点滴。花了一个半的夜晚,完成了此文。

本文献给为我们送雨伞而把皮鞋?湿了的猛人。


终于有机会重写这个话题了。我承诺读完本文,对Java如何实现不规则控件将会有非常清晰的认知。

什么是自省?作为一个做技术的工程师,在以往学习和工作的过程中肯定曾经遇到过超级多的问题,其中有一些是棘手的,很难解决的,最终不了了之的问题。过了些时日以后,重新审视,看看以往的这些疑难问题现在是不是可以解决了,这就是一种自省的态度。即便是对于已经解决的问题,以及已经掌握的理论,重新审视看有没有更优的解法,有没有新的思想,这很必要。

缘由

2003年是我接触Java的第一年,2004年是我接触Java的第二年。

由于是自学Java,又是大专,没有科班的基础,所以最初不是很care算法和数据结构,因为Java可以快速作出一个肉眼可以看到的GUI,所以我选择了Java而不是C/C++,同时由于MFC这些和微软的系统强相关,也就是说,同时放弃了VC++。

但是直到2006年我都被一个问题所困扰,即:
如何用Java实现一个不规则的窗体或者控件?

之所以有这个问题,是因为我那时觉得像豪杰超级解霸这种播放器的 换肤 功能非常牛逼,比较有趣,如果说做GUI,当然不能少了这个。让人惊奇的是,一个播放器界面竟然可以是一个如下般非规则的样式:
在这里插入图片描述
这太帅了!我也想用Java做。

但是Java的界面只能是那种四四方方的样子, 如何能把这种四四方方的界面裁剪成类似上面的样子 ,这是我进入这个这个行业以来 第一个要解决的问题。

我当时使用的Java SDK版本是J2sdk 1.4.2版本,经典版本。

2006年12月Java 1.6发布之前,网上根本搜不到答案,我自己也是有时间就折腾,几乎把Swing/AWT的API都撸遍了,也没有结果。最终,我是通过Eclipse的SWT解决了问题。

Eclipse的SWT让很多有同样需求的人眼前一亮。我的第一份工作就是基于SWT开发的。

是的Eclipse SWT有可以切割Java窗体和控件的API,具体例子我就不说了,网上一搜一大片。问题是,我想用标准的J2SE来做这些啊!

后来,我是通过JNI的方式,调用自己写的本地C/C++库来完成切割窗体的。我也是因为要写这个本地C代码,从而 顺便 开始学习C语言编程。

这种功能用本地代码写是很容易的,比如在微软的Windows系统,就可以用Windows API来做,而在Linux系统,则可以用基于X11的Gnome,KDE来完成,毕竟本地的GUI库必然要支持这些功能。

然而Java作为其承诺的 一次编写,到处运行 的跨平台语言,不可能用同一个接口去兼容不同操作系统平台的不同GUI效果,所以Java也就根本就没有提供这样的功能。

Java的公共API只能完成所有操作系统平台都支持的操作,并且还要保证达到同样的效果,至少不能差太多。我们知道Windows和UNIX/Linux的X11完全就不是一个东西,甚至都不是一类东西…

Java实现不规则控件这件事

这里还需要再PS一下。

关于Java实现不规则控件这件事,很多人纠结过,实际上Java作为承诺的跨平台语言,它的优势根本就不在开发GUI程序,GUI这种是严重依赖本地系统的。

虽然Eclipse的SWT曾经给人眼前一亮的的感觉,但它依然不是万金油。

SWT不需要显式的进行JNI操作,不需要引入额外的包,处在集成开发环境Eclipse本身,直接拿来就能用。但是它只是将JNI隐藏了。说白了,SWT之所以可以做出和本地系统几乎一样的GUI控件,它所做的就是 将Win32 API,X11 API这种本地API封装成了Java的API ,是的,说白了就是一层封装,没什么大不了的。

在Java SE 1.6之前,Java AWT/Swing要想做不规则控件,需要你自己去折腾JNI,而SWT替你把这一切封装好了,这才是它的唯一优势。

这不,Java SE 1.6出来后,SWT的优势就不在了。因为它在Java内部做了同样的事情。


使用Java SE 1.6如何实现不规则窗口

到了2006年12月份,Sun放出了Java SE 1.6,带来了福音!

Java SE 1.6提供的 com.sun.awt.AWTUtilities 和Eclipse SWT一样,封装了JNI操作,让程序员可以直接使用,就像不知道JNI这回事一样。

在这之后,当我们再次搜索 如何用Java实现不规则窗体 这个话题时,答案就有了,但是基本都是同一个答案:
通过com.sun.awt.AWTUtilities提供的方法来完成!

比如:

com.sun.awt.AWTUtilities.setWindowShape(Window window, Shape shape);

只需要初始化一个Shape对象,就能实现一个任意形状的窗口,我以圆形窗口为例:

import java.awt.*;
import javax.swing.*;
import java.awt.geom.Ellipse2D;

public class CircleFrame extends JFrame {
	public CircleFrame () {
		super("Triangle Frame");
		setSize(500,500);
		setUndecorated(true);
	}

    public static void main(String[] args) {
	CircleFrame frame = new CircleFrame();
	frame.setVisible(true);
	com.sun.awt.AWTUtilities.setWindowShape(frame, new  Ellipse2D.Double(0, 0, 500, 500));
    }

}

效果如下:
在这里插入图片描述
但是这明显不是我想要的,折腾了小两年,最后新版本提供了一行代码搞定,感觉受到了伤害。并且,在编译CircleFrame的时候,提示如下:

root@name-VirtualBox:~# javac CircleFrame.java
CircleFrame.java:16: warning: AWTUtilities is internal proprietary API and may be removed in a future release
	com.sun.awt.AWTUtilities.setWindowShape(frame, new  Ellipse2D.Double(0, 0, 500, 500));
	           ^
1 warning

但这种API是完全无法把控的,明显这种API使用JNI的方式实现特定的功能,严重依赖了平台,并且API本身提供方拥有最终解释权,换句话说,使用这种API是危险⚠️的,搞不好哪天就废弃或者改变了!毕竟com.sun的包嘛…不稳定,现如今这公司都不在了,唏嘘。


正确的做法,当然是 不依赖API,所有事情自己做了。

Java AWT 和 Java Swing


当初在学习Java的时候,了解到Java提供了两种GUI API:

  • Java AWT
  • Java Swing

我当时特意了解到这两者的本质不同:

  • Java AWT
    调用操作系统GUI平台原生的操作来生成控件,换句话说,AWT的容器,控件只是操作系统GUI容器,控件在JVM里面的一个影子,AWT操作的是系统原生的容器,控件,它们称作AWT组件的 本地同位体
    优势:效率高。
    劣势:依赖底层,不同系统的观感会不同。
  • Java Swing
    除了顶层容器,其它的控件都是不依赖底层操作系统GUI平台的,换句话说,Swing的容器和控件是JVM自己画出来的。
    优势:跨平台,因为画法是一样的。
    劣势:效率低,慢。

不管怎样,本文将用上述AWT和Swing分别采用的两种方法,即调用底层的GUI操作以及自己画,来实现不规则的窗体和控件:

  • 不规则窗体将实现一个三角形的窗体。
  • 不规则控件将实现一个三角形按钮安装在上述三角形窗体中。

Java AWT实现不规则控件

先看AWT的Frame如何来做。先看Java代码:

import java.awt.*;
import java.awt.event.*;
public class AWTDemo extends Frame {

	// 三角形按钮btn
	Button btn;
	//本地方法,用来将窗口和按钮切割成三角形
	private native void cutWindow(String title);
	static {
		System.loadLibrary("cutWindow");
	}

	public AWTDemo(String title, String btn_title) {
		super(title);
		setSize(500,500);
		// 果断去掉窗口上方的标题栏
		setUndecorated(true);

		// 生成一个四四方方的按钮对象
		btn = new Button(btn_title);
		// 按钮事件处理,第一次按下换个字,第二次按下退出
		btn.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				if (e.getSource() instanceof Button) {
					Button bu = (Button)e.getSource();
					if (bu.getLabel().equals("AWT button"))
						bu.setLabel("To Exit");
					else
						System.exit(0);

				}
			}
		});
		// 黄色按钮
		btn.setBackground(Color.YELLOW);
		add(btn);
	}

	public static void main(String args[]) {
		String title = "abc";
		String btnpeer = "sun-awt-X11-XButtonPeer";
		String btn_title = "AWT button";
		// 实例化一个Frame对象
		AWTDemo frame = new AWTDemo(title, btn_title);
		// 显示它
		frame.setVisible(true);
		try {
			// 至此为止一切都是四四方方的
			Thread.sleep(2000);
			frame.cutWindow(title);
			// 窗口成了三角形
			Thread.sleep(2000);
			frame.cutWindow(btnpeer);
			// 按钮也成了三角形
		} catch (Exception e) {
		}
	}
}

先看效果,再说JNI代码:
在这里插入图片描述
点击一下三角形的黄色按钮:
在这里插入图片描述

这是如何实现的呢?嗯,这是调用了本地方法cutWindow,要理解cutWindow的逻辑,这里简单说点X Window的东西,如果是在Windows平台做实验,那么就需要了解下Windows API了。

X Window系统是一个超级复杂的C/S模式的GUI系统,其实它的架构是比较简单的,类似我们远程登录的Telnet/SSH这些。我来类比一下:

  • Telnet/SSH
    你在 负责输入输出显示的客户端 上用 键盘 敲入字符以及控制键,字符序列传输至Telnet/SSH服务器处理,然后服务器回复另一字符序列,Telnet/SSH客户端收到后解释它们,以便按照协议规范在终端 回显字符,最终生成一个CLI。
  • X window
    你在 负责输入/输出和显示的X服务器 上用 键盘,鼠标 执行一系列操作,这些操作被X服务器传输至X客户端处理,然后X客户端按照X协议规范回复数据序列,X服务器收到后解释它们,以便按照X协议规范决定 在X服务器的Display 显示器哪个像素绘制什么颜色,最终生成一个GUI。

它们非常类似,简直是一个逻辑。唯一不同的是,X window貌似和Telnet/SSH的客户端/服务器是反着的。事实上理解起来很简单,毕竟X服务器就是用来显示的嘛。

X Window系统封装了一系列的容器,控件,比如画布,按钮,复选框…X客户端可以以这些封装好的容器,控件为单位进行操作。

深入的X window本文不谈,现在回到Java AWT程序。

刚才说了,每一个AWT容器或者控件,都只是操作系统本地GUI的一个容器或者控件的影子,那么 如果我们想操作这个AWT对象,就要找到它的本地对象!

我们用X系统的实现之一X11提供的 xwininfo 命令来探究一下内外。关于xwininfo,了解下面的就够了:

0.你可以把display 理解成一个显示器加上一套鼠标/键盘的套件。
1. X系统一个Display对象的所有窗体控件均按照Tree形式嵌套组织,比如一个Frame上的Button就是该Frame的child,这与Linux的进程组织非常类似。
2. xwininfo可以枚举系统当前Display上的所有的窗体,并且给出其组织关系。

简单修改一下代码,做一个 AWTDemo2.java ,去掉那些本地方法调用,仅仅保留主干:

import java.awt.*;
public class AWTDemo2 extends Frame {

	Button btn;

	public AWTDemo2(String title, String btn_title) {
		super(title);
		setSize(500,500);
		setUndecorated(true);

		btn = new Button(btn_title);
		add(btn);
	}

	public static void main(String args[]) {
		String title = "abc";
		String btn_title = "AWT button";
		AWTDemo2 frame = new AWTDemo2(title, btn_title);
		frame.setVisible(true);
	}
}

先执行 java AWTDemo2 打开AWT Frame界面,然后我们通过 xwininfo 看看能不能找到我们的AWT对象。

root@name-VirtualBox:~# xwininfo -tree -root
	...
           # 下面的Frame就是我们的AWT Frame
     0x1c00007 "abc": ("sun-awt-X11-XFramePeer" "AWTDemo2")  500x500+67+27  +67+27
        2 children:
        0x1c0001f "FocusProxy": ("Focus-Proxy-Window" "FocusProxy")  1x1+-1+-1  +66+26
        0x1c0001c "Content window": ("sun-awt-X11-XContentWindow" "AWTDemo2")  500x500+0+0  +67+27
           1 child:# 下面的这个child就是我们的AWT Button
           0x1c00020 "sun-awt-X11-XButtonPeer": ("sun-awt-X11-XButtonPeer" "AWTDemo")  500x500+0+0  +67+27
     ...

果真是找到了:

0x1c00007 "abc": ("sun-awt-X11-XFramePeer" "AWTDemo2")  500x500+67+27  +67+27
0x1c00020 "sun-awt-X11-XButtonPeer": ("sun-awt-X11-XButtonPeer" "AWTDemo2")  500x500+0+0  +67+27

Java代码里没有任何与外界的交互,但是X系统里还是找到了。所以说:
Frame/Button这种AWT组件对于X系统是可见的,同样在Windows系统上,它也是可见的!

如果我们换成Swing控件呢?相同的布局,只是将Frame换成JFrame,将Button换成JButton,如何呢?让我们试一下:

import javax.swing.*;

public class SwingDemo2 extends JFrame {

	JButton btn;

	public SwingDemo2(String title) {
		super(title);
		setSize(500,500);
		setUndecorated(true);

		btn = new JButton("Swing button");
		add(btn);
	}

	public static void main(String args[]) {
		String title = "abc";
		SwingDemo2 demo = new SwingDemo2(title);
		demo.setVisible(true);
	}
}

用java SwingDemo2执行的同时,xwininfo的结果如何呢?看一下:

     0x2000007 "abc": ("sun-awt-X11-XFramePeer" "SwingDemo2")  500x500+67+27  +67+27
        2 children:
        0x200001f "FocusProxy": ("Focus-Proxy-Window" "FocusProxy")  1x1+-1+-1  +66+26
        0x200001c "Content window": ("sun-awt-X11-XContentWindow" "SwingDemo2")  500x500+0+0  +67+27

Content window不再有Button这个child了!那么这个JButton到底在哪里呢?

JButton对于X系统是不可见的,同样在Windows系统上,它也是不可见的!Java Swing除了顶层容器,其它都是自己在JVM里画大的,JVM系统外不可见!

这下就清晰多了!我们通过xwininfo彻底理解了AWT和Swing到底区别在哪里了!


是时候上JNI调用的本地代码了!这是用C写成的,基于X11编写。

所谓的 本地代码 ,顾名思义就是跨平台解释执行的Java JVM之外的不受JVM控制的代码,脱离Java规范的约束 。这种支持在Java规范里叫做JNI。类似内联汇编对C语言的降维打击(内联汇编不受外部C语言规范的约束!)一样,本地代码也能对Java代码实施降维打击。所以如果你写了本地代码,你一定要知道你在干什么,通过Java的调试方法是无法知道本地代码的细节的!

接下来,若要Java代码和本地代码对接起来,需要一个 接口 ,该接口就是通过 javah AWTDeom 生成的一个 AWTDemo.h 的C/C++头文件:

#include <jni.h>

JNIEXPORT void JNICALL Java_AWTDemo_cutWindow
  (JNIEnv *, jobject, jstring);

接下来就是实现 Java_AWTDemo_cutWindow 函数了。

JNI规范要求单独做一个动态连接库,在Linux系统,就是一个叫做 libAWTDemo.so 的动态库文件,注意,文件名一定要有 lib 前缀,因为Linux加载器就是这样加载库的。

我下面直接给出带有注释的 cutWindow.c 文件:

#include <X11/Xos.h>
// 这个shape extension非常重要
#include <X11/extensions/shape.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include "AWTDemo.h"

// 该函数执行根据名字查找组件句柄的逻辑,不解释,详情参见X11编程文档。
// 如果是Windows系统,那么就是另一种方式获取组件的句柄了。
// 注意:这个例子并没有借助使用AWT本地同位体的概念,所以需要手工自己查找
Window findWindowByName(Display *dpy, Window parent, char *name)
{
	unsigned int num, i;
	Window *subwindow, dummy;
	Window result = 0;
	char *title;

	// 按照名称查找,如果名称匹配,就返回当前的组件。
	XFetchName(dpy, parent, &title);
    	if (title && !strcmp(name, title)) {
		return parent;
	}

	// 否则递归查找其所有的children
	if (!XQueryTree(dpy, parent, &dummy, &dummy, &subwindow, &num))
		return 0;

	for (i = 0; i < num; i ++)  {
		result = findWindowByName(dpy, subwindow[i], name);
		if (result) {
			goto free_and_ret;;
		}
	}

free_and_ret:
	if (subwindow)
		XFree ((char *)subwindow);
	return result;
}

JNIEXPORT void JNICALL Java_AWTDemo_cutWindow (JNIEnv *jenv, jobject o1, jstring title)
{
    Window window;
    Display *dpy;
    Region region;
    XPoint p[3];
    XPoint pbtn[3];

   unsigned char *name = NULL;
   // 首先需要把Frame或者Button的title通过JNI传递到C/C++代码里
   name = (*jenv)->GetStringUTFChars(jenv, title, 0);

    dpy = XOpenDisplay(":0");
    if (!dpy) {
        exit(1);
    }

	// 按照X11的API文档查找名字为name的容器或者控件,也就是做类似于xwininfo命令做的事情
	// 这实际上是一个枚举所有组件的过程。
	// 事实上,可以通过AWT本地同位体将window句柄通过参数传递过来的。为了不引入同位体的概念,暂时不考虑。
    window = findWindowByName(dpy, DefaultRootWindow(dpy), name);
    if (!window) {
        exit(1);
    }
	// Frame的三角形外形三点定义
    p[0].x = 250; p[0].y = 125;
    p[1].x = 450; p[1].y = 425;
    p[2].x = 50; p[2].y = 425;
	
	// Button的三角形外形三点定义
    pbtn[0].x = 250; pbtn[0].y = 190;
    pbtn[1].x = 320; pbtn[1].y = 290;
    pbtn[2].x = 180; pbtn[2].y = 290;

    if (!strcmp(name, "abc")) { 
    	// Frame的区域
    	region = XPolygonRegion(p, 3, EvenOddRule);
    } else {
    	// Button的区域
    	region = XPolygonRegion(pbtn, 3, EvenOddRule);
    }
    // 设置组件的外形
    XShapeCombineRegion(dpy, window, ShapeBounding, 0, 0, region, ShapeSet);
	// close该Display时会flush/repaint组件。
    XCloseDisplay(dpy);
}

代码写好了,接下来看看如何将其做成动态链接库。JNI的规范里对这个步骤也有说明,我下面直接给出:

root@name-VirtualBox:~# gcc -fPIC -c -I"/usr/lib/jvm/java-8-openjdk-amd64/include" -I"/usr/lib/jvm/java-8-openjdk-amd64/include/linux" cutWindow.c -o cutWindow.o
root@name-VirtualBox:~# gcc -shared -o libcutWindow.so cutWindow.o

OK!现如今,该例子的AWTDemo.class运行所需的所有依赖都已经准备好了,我们希望JVM在当前路径下去加载 libcutWindow.so

root@name-VirtualBox:~# javac AWTDemo.java
root@name-VirtualBox:~# java -Djava.library.path=. AWTDemo

执行起来就是上面图示的效果!

本地代码是通过控件的Title来查找本地同位体的,如果是Windows平台,查找组件的操作要简单的多,只有有API可用:

HWND FindWindowA(
  LPCSTR lpClassName,
  LPCSTR lpWindowName
);

详情参见:https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-findwindowa
【PS:我十多年前的不规则窗口版本就是在Windows平台做的,用的就是上面这个FindWindowA】

事实上可以通过参数将本地同位体的控件句柄直接从Java代码传递到本地代码中的,这个后面再说。

上述的本地代码动态库是基于X11实现的,我本来想用GTK,KDE来做例子,然而这些并不是原汁原味的,会引入很多额外的东西,比如看了GTK代码后,就会纠结于GTK是怎么回事…我们是在做Java不规则控件,而不是在学习GTK。所以越底层越好。

在实现中,代码中使用了X11的Shape extension,这是一个扩展,并不包含在原生的X11中,基于它可以实现不规则的窗口和控件,它非常复杂,详细参见:
https://en.wikipedia.org/wiki/Shape_extension
https://www.x.org/releases/X11R7.7/doc/xextproto/shape.html
本文不赘述,还是那句话,Java以外的,点到为止。

AWT版本的自定义不规则组件的介绍就是这么多。接下来看看Swing的版本。


Java Swing实现不规则控件

前面说了,Java Swing除了顶层的容器,其它的控件组件都是自己画出来的,那么要实现不规则的JFrame,作为顶层容器的JFrame,由于其依然是映射到本地的真实窗口,所以依然是上面的JNI的方法去切割。

重点是JButton的不规则化如何来做。换句话说,如何把它 画出来

JButton在本地的GUI系统里根本就找不到,就像上面xwininfo展示的结果那样,怎么办?

画一个就是了!

如何来画呢?我还是直接给出完整的代码吧, SwingDemo.java 列如下:

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

// 从JButton派生一个子类,实现三角形JButton
class triangleButton extends JButton {
	// 此Button的三角形区域
	Polygon triangle;
	triangleButton(String title, int x[], int y[]) {
		super(title);
		// 根据参数初始化三角形区域
		triangle = new Polygon(x, y, 3);
		// 不显示按钮边框!完全由我们自己绘制的三角形来决定
		setBorderPainted(false);
		setContentAreaFilled(false);
	}

	// 重写paintComponent方法,区分按钮按下和释放时显示不同的颜色,显得逼真!
	public void paintComponent(Graphics g) {
		if (this.getModel().isPressed()) {
			g.setColor(Color.BLACK);
		} else {
			g.setColor(Color.LIGHT_GRAY);
		}
		// 用不同的颜色画同一个三角形
		// 如果能细化边缘凸凹高亮,那就更美观了!但是那样代码太长。
		g.fillPolygon(triangle);
		super.paintComponent(g);
	}

	// 重写contains,判断鼠标当前的焦点是不是属于该按钮的区域范围内。
	// 这是Java Swing的创举,委托UI管理器来实现范围限定约束的托管!
	public boolean contains(int x, int y) {
		if (triangle.contains(x, y))
			return true;
		return false;
	}
}

public class SwingDemo extends JFrame {
	JButton btn;
	// 定义按钮三角形的三点
	int button_x[] = {250, 315, 185};
	int button_y[] = {327, 227, 227};

	// 用户切割JFrame的本地方法
	private native void cutWindow(String title);
	static {
		System.loadLibrary("cutWindow");
	}

	public SwingDemo(String title) {
		super(title);
		setSize(500,500);
		setUndecorated(true);

		btn = new triangleButton("Swing button", button_x, button_y);
		btn.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				if (e.getSource() instanceof triangleButton) {
					triangleButton bu = (triangleButton)e.getSource();
					if (bu.getText().equals("Swing button"))
						bu.setText("To Exit");
					else
						System.exit(0);

				}
			}
		});
		add(btn);
	}

	public static void main(String args[]) {
		String title = "abc";
		SwingDemo demo = new SwingDemo(title);
		demo.setVisible(true);
		// 切割JFrame为三角形,同AWTDemo
		demo.cutWindow(title);
	}
}

当执行 java SwingDemo 时,展示一下效果:
在这里插入图片描述
点击一下试试看,点击的瞬间,三角形变成了黑色,这个太快了,没法截屏,但是松开鼠标后,按钮的字变了:在这里插入图片描述
再点一下,如代码逻辑所示,退出。

别看这是Swing画出来的,但 这是真正的三角形按钮,不仅仅是视觉上的,你只有点击那个小三角形区域,才会有效果,别的区域是不行的。

Swing版本的不规则窗口和按钮总结下来就是:

  • Swing窗口依然采用JNI的方式在X11 API实现的动态库里进行切割;
  • Swing窗口上的按钮,自己编码绘制完成不规则化。

纯Java的完整例子(?窗口,?按钮)

能不能不用JNI?

可以的。

不用JNI实现窗口切割,不用说也能猜到原理,Java的工具包自己帮你JNI了呗。

当我们已经理解了上述细节后,表示可以完全hold住java的api后,便可以自由使用一开始我并不提倡的Java自带的com.sun.awt.AWTUtilities了。

com.sun.awt.AWTUtilities ,它就是用JNI实现窗口切割的。那么我便使用它直接来完成一个不规则窗口,不然我还要自己写本地代码,我又不想深入去学习X11。那么OK,我直接用AWTUtilities了!

这个例子中,我使用一双大皮鞋图片作为窗口,一双小皮鞋图片作为按钮,窗口的形状是大皮鞋,按钮的形状是小皮鞋,皮鞋是不规则的,所以窗口和控件是不规则的。

代码只有一个文件, SkinShoeDemo.java ,列如下:

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import javax.imageio.ImageIO;
import java.net.URL;
import java.io.*;

// 实现不规则的小皮鞋按钮
class LittleSkinShoeButton extends JButton {
	ImageIcon img;
	BufferedImage imgpixe;
	LittleSkinShoeButton(ImageIcon img, String icon){
		super();
		this.img = img;
		setBorderPainted(false);
		setContentAreaFilled(false);
		setSize(img.getIconWidth(),img.getIconHeight());
		try{
			// 需要完整的图片像素来确定哪些像素属于皮鞋,哪些像素不属于皮鞋,这个涉及到“抠图”,后面会讲
			imgpixe = ImageIO.read(SkinShoeDemo.class.getResource(icon));
		} catch (Exception e){
			System.exit(0);
		}
	}

	// 当鼠标点击小皮鞋按钮“皮鞋区域”内部时,要展示出被点击的样子来,向下凹陷一下。
	public void paintComponent(Graphics g){
		if(this.getModel().isPressed()){
			// 向下凹陷5个像素,向右平移5个像素,感觉像是被点击了。
			g.drawImage(img.getImage(), 5, 5, this);
		}else{
			// 不被点击时,保持在原来的位置。
			g.drawImage(img.getImage(),0,0,this);
		}
	}

	// 重写contains方法。
	public boolean contains(int x,int y){
		int rgb,alpha;
		try{
			rgb = imgpixe.getRGB(x,y);
			// 获取像素的alpha值,如果是被“抠去”的,就不属于皮鞋内部。
			// 当初制作皮鞋图的时候,不是皮鞋范围的都“抠掉成透明”的了。
			alpha = (rgb>>24) & 0xFF;
			if(alpha != 0){
				System.out.println("属于小皮鞋范围,点击有效:[" + x + "," + y );
				return true;
			}
		}catch(ArrayIndexOutOfBoundsException e){
		}
		System.out.println("不属于小皮鞋范围,点击无效:[" + x + "," + y );
		return false;
	}
}

// 实现JFrame的背景图片,即一双大皮鞋。
class SkinShoePanel extends JComponent {
    private Image image;
    public SkinShoePanel(Image image) {
        this.image = image;
    }
	
	// 重绘,也就是画大皮鞋
    protected void paintComponent(Graphics g) {
        g.drawImage(image, 0, 0, this);
    }
}

// 大皮鞋窗口主类
public class SkinShoeDemo extends JFrame {

	JButton btn;

	private JPanel pixieimgPanel;
	public SkinShoeDemo(BufferedImage img, ImageIcon btn_img, String icon) {
		super("SkinShoe");
		setSize(img.getWidth(), img.getHeight());

		btn = new LittleSkinShoeButton(btn_img, icon);
		btn.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				if (e.getSource() instanceof LittleSkinShoeButton) {
					// TODO
				}
			}
		});
		this.setContentPane(new SkinShoePanel(img));
		btn.setLocation(280, 280);
		this.setUndecorated(true); // 这个必须调用
		// 按钮添加在画布上。
		this.getContentPane().add(btn);
	}

	public Shape getSkinshoeShape(BufferedImage pixieimg) {
		Area pixie = new Area();
		int width;
		int height;
		int x,y, temp; // temp起到了优化的作用,批量添加区域
		int rgb, alpha;

		width = pixieimg.getWidth();
		height = pixieimg.getHeight();
		for (y = 0; y < height; y++)  {
			temp = 0;
			for (x = 0; x < width; x++) {
				rgb = pixieimg.getRGB(x, y);
		 		alpha = (rgb>>24)&0xFF;
		 		/* 下面的if-else语句的含义就是下面注释版的if语句的优化版,即:
		 		 * 将“不透明”的像素拼接成一个“区域”。
		 		 * 所有“不透明”的像素就是在抠图时没有被抠掉的像素。
		 		 * 如果是使用下面注释版本的话,要一个像素一个像素添加,那么大一双皮鞋,要两分钟!!
		 		 * if (alpha != 0) {
		 		 *		Rectangle temppixe = new Rectangle(x, y, 1, 1);
				 *		pixie.add(new Area(temppixe));
		 		 * }
		 		 */
		 		if(alpha != 0) {
					if (temp == 0)
						temp = x;
				} else {
					if (temp != 0) {
						Rectangle temppixe = new Rectangle(temp, y, x - temp, 1);
						pixie.add(new Area(temppixe));
						temp = 0;
					}
				}
			}
		}
		return pixie;
	}

	public static void main(String args[]) {
		// 22.png就是哪个小皮鞋
		String pixieicon = "22.png";
		// 11.png是那双大皮鞋
		File file = new File("11.png");
		BufferedImage pixieImage = null;
		ImageIcon button_icon = null;
		try {
			pixieImage = ImageIO.read(file);
			button_icon = new ImageIcon(SkinShoeDemo.class.getResource(pixieicon));
		} catch (Exception e) {
		}
		SkinShoeDemo demo = new SkinShoeDemo(pixieImage, button_icon, pixieicon);
		demo.setVisible(true);
		// 切割吧!
		com.sun.awt.AWTUtilities.setWindowShape(demo, demo.getSkinshoeShape(pixieImage));
	}
}

看看效果呗:
在这里插入图片描述
只要不是大皮鞋区域,鼠标点击都是透明的:
在这里插入图片描述

这是真正意义的 不规则窗口


关于抠图

我现在简单说一下这个效果的前置要求,必须对两双皮鞋的图片进行 抠图 预处理。

先来看一下定义窗口外观的大皮鞋原图:
在这里插入图片描述
显然,按照常规,这是一张 四四方方 的图,矩形的。也就是说,这张图包含了 皮鞋前景白色背景 两个部分。如果拿这个图做我们的不规则窗口的11.png,将会是失败的,因为那个白色的背景并不会由于其Alpha值(含义马上会讲,这里只是代码的观感)等于0而被排除在有效范围之外:

rgb = pixieimg.getRGB(x, y);
alpha = (rgb>>24)&0xFF;

Alpha的值等于0而被放逐在我们需要的有效区域以外,但是原图的矩形区域内包括背景在内的所有像素的Alpha值均不为0,为什么?

我们单看白色的背景,换句话讲,白色背景的特性如下:

  • 颜色:有颜色,白色
  • 透明度:不透明(是的,它并不透明!)

一张图片的每一个像素,均会包含上述两个性质,一个颜色,一个透明度。这两个性质被保存在一个4字节的数字里,其中的颜色占据3个字节,分别保存三原色的各自分量,余下的1个字节保存透明度信息,很巧妙。

最常用的定义,4个字节定义如下:

typedef struct RGB_info {
u8 rgbBlue; 	// 蓝色分量
u8 rgbGreen; 	// 绿色分量
u8 rgbRed; 		// 红色分量
u8 rgbAlpha; 	// 透明度
} RGB。

这个叫做 带Alpha通道的RGB24 标准。

很少有图片在生成的时候就会设置某些像素的Alpha为完全透明,所以这个需要我们自己来 ,把背景抠掉即可,所谓的抠掉,就是将背景像素的Alpha值设置为0。

一般抠图程序很容易写,但是比较麻烦,原理很简单, 只要能通过RGB颜色信息识别出前景和背景,那么把背景像素的Alpha设置为0即可。

问题是如何识别。在我们这个例子中,很简单,把白颜色的给设置透明即可,但是有些像素并不是纯白,而是 接近白,灰白… 这让程序去定义一个范围吗?似乎这个范围如何来界定又是一个问题,最终就陷入了AI,哦,很高大上的领域!

还是用肉眼识别吧,一切抠图者自己来决定。这就要使用抠图工具了。这种工具一般让你自己用圈点指针自己标示那些部分要设置为透明,比如套索,画笔之类的。

昨晚让老婆用美图秀秀给帮忙把皮鞋前景给抠出来,但是没有成功,后来我找了一个在线的工具:
https://ue.818ps.com/clip/
在这里插入图片描述
还算挺方便的。反正也只是用一次,足够了。

最终把那双要做按钮的小皮鞋也完成了抠图:
在这里插入图片描述
用这两张图就可以制作不规则窗口和不规则按钮了。

最后让我们的SkinShoeDemo换一张图,看看效果:
在这里插入图片描述
给包括小小在内的演示了这个之后,都说好。


X11的畅想

本来我是想用JNI调用X11实现的动态库来实现这个 大皮鞋?窗口 的,但是失败了。我一直以为这是很容易成功的。

当初既然我可以用三个点来定义一个三角形,我就自然而然想到可以用 皮鞋轮廓的N个像素点 来定义一个N边形来 模拟大皮鞋?围绕着的区域 ,N是多少呢?就看大皮鞋图案的外轮廓有多少个点了。这个想法很合理。

首先我要先得到大皮鞋的轮廓。

我用下面的代码获得,命名为Outline.java:

import java.awt.image.*;
import java.io.*;
import javax.imageio.ImageIO;

public class Outline {
	static File src = null; 
	static File dst = null;
	static BufferedImage img = null;
	static BufferedImage outline = null;

	public static void main(String[] args) throws IOException {
		int i, j, width, height;
		int rgb, rgb1, rgb2, a1, a2, a3;
		src = new File(args[0]);
		dst = new File("outline_" + args[0]);
		img = ImageIO.read(src);
		width = img.getWidth();
		height = img.getHeight();

		outline = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
		// 纵向扫描
		for (i = 1; i < width - 1; i++) {
			for (j = 1; j < height-1; j++) {
				// 捕捉变化
				rgb = img.getRGB(i, j);
				rgb1 = img.getRGB(i + 1, j);
				rgb2 = img.getRGB(i - 1, j);
				a1 = (rgb>>24)&0xFF;
				a2 = (rgb1>>24)&0xFF;
				a3 = (rgb2>>24)&0xFF;
				if(a1 != 0 && (a2 == 0 || a3 == 0)) {
					// 为了轮廓线清晰,特使用白色来描绘“边界像素周边的四个点”
					rgb |= 0xffffffff;
					outline.setRGB(i, j, rgb);
					outline.setRGB(i, j - 1, rgb);
					outline.setRGB(i, j + 1, rgb);
					outline.setRGB(i + 1, j, rgb);
					outline.setRGB(i - 1, j, rgb);
				}
			}
		}

		// 横向扫描
		for (i = 1; i < height - 1; i++) {
			for( j = 1; j < width-1; j++) {
				// 捕捉变化
				rgb = img.getRGB(i, j);
				rgb1 = img.getRGB(i, j + 1);
				rgb2 = img.getRGB(i, j - 1);
				a1 = (rgb>>24)&0xFF;
				a2 = (rgb1>>24)&0xFF;
				a3 = (rgb2>>24)&0xFF;
				if (a1 != 0 && (a2 == 0|| a3 == 0)) {
					// 为了轮廓线清晰,特使用白色来描绘“边界像素周边的四个点”
					rgb |= 0xffffffff;
					outline.setRGB(i, j, rgb);
					outline.setRGB(i, j + 1, rgb);
					outline.setRGB(i, j - 1, rgb);
					outline.setRGB(i + 1, j, rgb);
					outline.setRGB(i - 1, j, rgb);
				}
			}
		}

		ImageIO.write(outline, "png", dst);
	}
}

以上这个程序,当我用大皮鞋图片的名称 11.png 作为参数执行时,会输出一张轮廓图片 outline_11.png ,输出图片效果如下图所示:
在这里插入图片描述
嗯,是一双皮鞋?!!

我用这个轮廓干什么呢?我要得到一个数组啊。于是我在上述代码的 outline.setRGB 处打印一个序列:

int idx = 0;
...
System.out.println("outl[" + idx + "].x=" + i + "; out[" + idx + "].y=" + j + ";");
idx ++;

打印的结果就是:

root@name-VirtualBox:~# java Outline 11.png |more
outl[0].x=29; out[0].y=722;
outl[1].x=29; out[1].y=723;
outl[2].x=29; out[2].y=724;
outl[3].x=29; out[3].y=725;
outl[4].x=29; out[4].y=726;
outl[5].x=29; out[5].y=728;
outl[6].x=29; out[6].y=729;
outl[7].x=29; out[7].y=730;
outl[8].x=29; out[8].y=731;
outl[9].x=29; out[9].y=732;
...
outl[1567].x=772; out[1567].y=293;

我将这个打印结果重定向到一个文件中,然后将其include到JNI的本地代码。

我的意图是希望这些 XPoint 可以生成一个 1568边形! 我希望它们可以定一个Region,然后让X11 shape去框定窗口的裁剪范围:

outl[9].x=29; out[9].y=732;
...
outl[1567].x=772; out[1567].y=293;
	
region = XPolygonRegion(outl, 1568, EvenOddRule);
XShapeCombineRegion(dpy, window, ShapeBounding, 0, 0, region, ShapeSet);

初看这些操作,和 Java com.sun.awt.AWTUtilities.setWindowShape 的操作几乎是一样的,定义一个Region而已,我用1568边形来框定这个窗口的范围,可行啊!

然而事与愿违!

问题出在了 如何画N边形 这件事上。我们希望系统会 逐个地将我们的皮鞋外轮廓点连接起来,形成一个多边形 ,但系统如何解释 逐个地 ,这是问题。

请注意 XPolygonRegion 函数,它的最后一个参数确定了 如何判断一个点是否在该Region的内部。 它的取值只有两个:

The fill-rule defines what pixels are inside (drawn) for paths given in XFillPolygon() requests and can be set to EvenOddRule or WindingRule. For EvenOddRule , a point is inside if an infinite ray with the point as origin crosses the path an odd number of times. For WindingRule , a point is inside if an infinite ray with the point as origin crosses an unequal number of clockwise and counterclockwise directed path segments. A clockwise directed path segment is one that crosses the ray from left to right as observed from the point. A counterclockwise segment is one that crosses the ray from right to left as observed from the point. The case where a directed line segment is coincident with the ray is uninteresting because you can simply choose a different ray that is not coincident with a segment.

For both EvenOddRule and WindingRule, a point is infinitely small, and the path is an infinitely thin line. A pixel is inside if the center point of the pixel is inside and the center point is not on the boundary. If the center point is on the boundary, the pixel is inside if and only if the polygon interior is immediately to its right (x increasing direction). Pixels with centers on a horizontal edge are a special case and are inside if and only if the polygon interior is immediately below (y increasing direction).

此乃问题之所在了。fill-rule 的局限导致了皮鞋内部的点不一定被判定为 内部

虽然没能成功使用外轮廓数组构造一个Region调用XShapeCombineRegion完成不规则窗口的切割,但是我知道已经提供 com.sun.awt.AWTUtilities.* 的肯定是有办法做到的:

  • 要么自己调用JNI,加以适配
  • 要么直接返回UNSPPORTED

没空研究X11细节,GUI本来也就不是我的关注点。但偶尔,我依然会花点时间探究一下若干年前遗落的问题,这是改变不了的事实。


JNI直接操作AWT本地同位体

洋洋洒洒写到这里,相信已经把Java如何调用JNI或者自身的API实现不规则窗体说的很清楚了,但是还有点小遗憾。

我们看上文中引述的本地代码 cutWindow.c ,其中操作的window句柄是通过字符串Title查找而来的:

window = findWindowByName(dpy, DefaultRootWindow(dpy), name);
XShapeCombineRegion(dpy, window, ShapeBounding, 0, 0, region, ShapeSet);

如果启动了同样Title的两个实例,会怎样?这就不得不将这多个同样Title的window通过别的键值进行区分。

正确且直接的做法应该是Java直接将window句柄通过参数传递进来!这样就可以直接操作明确的window控件了!

问题是Java代码中如何获得本地同位体的句柄呢?理论上来讲,这个系统底层的概念在Java API的层面应该是不可见的,Java代码只认识 跨平台的Frame/JFrame 这种,不可能会认识 Windows的HWND,X11的Window 的!

然而,如果你不追求通用性,不追求跨平台(窗口不规则切割这件事本来就是平台相关的),办法嘛,必然是有的。我下面直接给出代码吧。

  1. 先看 PeerDemo.java:
import java.awt.*;
import java.awt.peer.*;
import sun.awt.X11.*;

public class PeerDemo extends Frame {

	private native void cutWindow(long display, long hwnd);
	static {
		System.loadLibrary("cutWindow");
	}
	WindowPeer peer = null;
	XBaseWindow base = null;
	long hwnd = 0;

	public PeerDemo() {
		setSize(500,500);
		setUndecorated(true);
		setVisible(true);
		this.peer = (WindowPeer)this.getPeer();
		this.base =(XBaseWindow)peer;
		this.hwnd = base.getWindow();
	}

	public static void main(String args[]) {
		PeerDemo demo = new PeerDemo();

		demo.cutWindow(XToolkit.getDisplay(), demo.hwnd);
	}
}
  1. 再看本地方法声明 PeerDemo.h:
JNIEXPORT void JNICALL Java_PeerDemo_cutWindow
  (JNIEnv *, jobject, jlong, jlong);
  1. 最后看本地动态库代码 cutWindow.c:
#include <X11/extensions/shape.h>
# include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <jni.h>

JNIEXPORT void JNICALL Java_PeerDemo_cutWindow (JNIEnv *jenv, jobject o1, jlong display, jlong hwnd)
{
	Window window;
	Display *disp;
	Region region;
	XPoint p[3];

	disp = (Display *)display;
	window = (Window)hwnd;

	p[0].x = 250; p[0].y = 100;
	p[1].x = 450; p[1].y = 425;
	p[2].x = 50; p[2].y = 425;

	region = XPolygonRegion(p, 3, EvenOddRule);
	XShapeCombineRegion(disp, window, ShapeBounding, 0, 0, region, ShapeSet);
}

非常简单的一气呵成,看看效果:
在这里插入图片描述
编译时的Warning是必然的,Java的Doc上已经说的很明白了:
在这里插入图片描述
getPeer意味着关联了本地,承诺跨平台的Java怎么可能暴露出这么低层次的接口呢。

结束了吗?嗯,差不多了。但是且慢!

还记得上文中我的失败吗?我想勾勒出皮鞋?的轮廓,然后把这些轮廓的像素点(超级多的点,至少好几千个吧)传递给本地代码,企图在本地代码中用这些轮廓点构建出一个X11的Region结构体:

region = XPolygonRegion(outline_points, 3188/*举个例子,或许更多吧*/, EvenOddRule);

然后呢,将其传递给X11的 XShapeCombineRegion 函数进行切割。然而遗憾的是,X11并不是如我希望的那般将这些点按照皮鞋的样子顺序连接起来成为一个皮鞋外形的,它有自己的连接方式。很遗憾,失败了(也许是我对X11不太了解,我暂时只能理解到这个程度)。

然而,X11提供了另一种 构建任意形状 的方法,即 组合矩形

这很好理解,既然所有的图像在计算机中都是一个个的像素组成的,而每一个像素就是一个长宽均为1的矩形,那么 任意形状至少可以用这么多像素矩形组合而成 。作为优化,还可以将矩形的数量减少到最少,这是一个算法问题,我这里仅仅给出一个思想。比如,依然是那个皮鞋的轮廓,矩形可以如下分割:
在这里插入图片描述
细微之处我没有画,反正就是这个意思。

X11提供了组合不同大小矩形的API,即:

void XShapeCombineRectangles (
	Display *dpy,
	XID dest,
	int destKind,
	int xOff,
	int yOff,
	XRectangle *rects,
	int n_rects,
	int op,
	int ordering);

参数很丰富,我还真没搞明白,详情可以参见X11 Shape Extension的文档:
http://www.xfree86.org/current/shape.pdf

不过给出个例子还是可以的。Java代码如下:

import java.awt.*;
import java.awt.peer.*;
import sun.awt.X11.*;
import javax.imageio.ImageIO;
import java.net.URL;
import java.io.*;
import java.awt.geom.*;
import java.awt.image.*;

public class PeerDemo extends Frame {

	private native void cutWindow(long display, long window);
	static {
		System.loadLibrary("cutWindow");
	}
	WindowPeer peer = null;
	XBaseWindow base = null;
	long hwnd = 0;

	public PeerDemo() {
		setSize(500,500);
		setLocation(200,300);
		setUndecorated(true);
		setVisible(true);
		this.peer = (WindowPeer)this.getPeer();
		this.base =(XBaseWindow)peer;
		this.hwnd = base.getWindow();
	}

	public static void main(String args[]) {
		PeerDemo demo = new PeerDemo();
		demo.cutWindow(XToolkit.getDisplay(), demo.hwnd);
	}
}

本地代码如下:

#include <X11/extensions/shape.h>
# include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <jni.h>

JNIEXPORT void JNICALL Java_PeerDemo_cutWindow (JNIEnv *jenv, jobject obj, 
										jlong display, jlong hwnd)
{
	Window window;
	Display *disp; 
	XRectangle rects[4];
	XRectangle *pRect = &rects[0];

	disp = (Display *)display;
	window = (Window)hwnd;

	// 我们组合下面4个矩形:
	rects[0].x = 0;
	rects[0].y = 0;
	rects[0].width = 100;
	rects[0].height = 100;
	rects[1].x = 100;
	rects[1].y = 0;
	rects[1].width = 100;
	rects[1].height = 50;
	rects[2].x = 200;
	rects[2].y = 0;
	rects[2].width = 10;
	rects[2].height = 400;
	rects[3].x = 0;
	rects[3].y = 100;
	rects[3].width = 10;
	rects[3].height = 300;

	XShapeCombineRectangles(disp, window, 
							ShapeBounding, 0, 0, pRect, 4, 
							ShapeSet, YXSorted);

}

效果如下图所示:
在这里插入图片描述
有点这个意思了。如果把这些矩形按照这个思路不断细化,就可以出现皮鞋外观了。


什么是编程

还记得 《Java Swing 2nd Edition》 这本书吗?

我当时买了中文版之后,太厚,切割成了3部分,然而最终还是没有读完,惭愧。现在它的代码资源放到github了:
https://resources.oreilly.com/examples/9780596004088
我记得第28章的那个圆形飞梭控件非常好,嗯,Java Swing自己定义的不规则控件,是的,它是Swing自己画出来的。当时非常震撼。

现在回头想想,有啥好震撼的呢,计算机屏幕上的所有像素不都是画出来的吗?关键是 如何画 才是根本,而这又涉及到了算法。

如果不知道JNI,很难用Java做出不规则控件,如果知道了JNI,至少知道了有个渠道可以做,然而此时你还必须懂Win32 API/X11 API这些,你才能真的动手去做。之后如果这些全都懂了呢?就以为自己可以任意画界面了吗?

不不不,这才到了关键的地方,这才是刚刚开始,比如上文中所说的,如何把皮鞋切割成数量最少的矩形,这比如何调用X11 API重要得多了,这里面奥妙太深了。

换句话说,你想画画,目前你只是买了本子,画笔,画板,并且知道了如何使用它们,这些都是 必先利其器 的事,并不是画画本身!

所以说呢,不是精通几个API,精通几门编程语言语法,精通几个工具的使用,就是精通编程了。甚至即便你精通很多系统底层的调试方法,你也不一定懂编程。你只是精通工具如何使用而已。

比如我,我精通如何摆置系统底层,如何摆置协议栈,但是这并不意味着我精通编程,编程的核心是算法,而不是如何摆置代码本身。

编程是一种如何组织逻辑的艺术,它不是一种如何使用编程语言的技术 编程语言只有一种用法,就是 用对它

换句话说,如果你精于如何组织逻辑,那么即便是使用自然语言,你也可以精通编程(这通常见于物理科学家,律师,外交官等职业)。编程语言只是实现这种逻辑组织的手段而已(所以生物学家,文学家很多也精于几种编程语言,大多数用于数据分析)。所以,一定要把 会编程语言会编程 区别开来。

以上形而上的说法如何形而下落地?这就要将行业的分工细化为 算法工程 两个方向。

算法侧重业务逻辑本身,而工程则是为了更好的组织算法,使得其符合工业约束,成为优质的产品。比如说可扩展性这个就是工程要做的事,而效率则大部分是算法的工作。

最后,我觉得我还是不会编程,在历经的学习和工作过程中,我掌握了系统的工作原理,底层的调试技巧,但是我依然吃力于编程,这意味着我的逻辑组织混乱,但是这并不是什么缺点更不是缺陷,这意味着我可以天马行空,而这正是定位棘手问题所需要的。所以说,摆正自己的位置最重要,不会编程没什么丢人的。


勘误:全文均没有出现 “一双皮鞋” ,仅仅是 “一只皮鞋” 而已!更正。

浙江温州皮鞋湿,下雨进水不会胖。

©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值