曾经接了一个设计公司的图库软件项目,设计师做设计时,经常要参考一些素材,这些素材由各设计师按类别上传到公司服务器给大家分享,长期积累下素材数量达到了上百个分类,上万副图片,总共几百G的文件,设计师靠windows访问服务器来进行素材的查阅,很难找到理想中的素材。
在架构的选型上,无非是BS和CS两种模式,BS模式是流行手法,但是文件上传功能始终觉得用户体验不好,同时也不支持按文件夹上传的功能,另一方面,客户端涉及到一系列的用户操作,而浏览器下的菜单开发也比较费劲,所以我采用了更传统的CS方式来实现图库,用java swing来实现图库的客户端,而服务端也开发相应的java服务。但swing开发对我来说是个比较大的挑战,首先没有实战经验,很多东西要现捣鼓,其次美工没法介入太多,很多效果都得自己调。但我相信一句话,那就是谁也不是生下来就能搞开发的,不会就学,所以挑战归根到底是个时间问题,不算难点。
那么图库最基本的需求就是一方面能快速检索,令一方面也能支持分类管理和图片素材的上传。这些素材中,很多都是非常精美的效果图,单一文件非常大,基本都在四五m左右,最大的有60多m,如果要支持网络下的快速检索,那就只能使用缩略图了,于是自然的采用了一个非常优秀的缩略图软件JMagick。
我列出了一些问题,并且敲定了相关方案:
如何实现文件上传?
既然选择了cs方式,只能自己实现文件上传了,也考虑了ftp的方式,但由于服务端需要将上传后的图片生成缩略图,ftp的服务端事件根本没法抓取,所以不如自己实现文件传输来得简单和可控。所以我采用了xsocket+自定义传输协议的方式来实现文件上传。
因为通讯上涉及到很多种操作的交互,比如请求缩略图、上传文件、图片管理(删除、重命名等)、请求原始图片(下载原始图片)等等一系列操作,所以协议上还是采用了xml的方式,但现在来看,xml的打包和解析实际上太重量级,改为json数据结构会更好一些。一个合理的图片上传包数据应该包含文件名、文件长度、要上传到的分组以及图片字节流等内容。
如何控制图片的展示?
用户要快速检索,那必须要求在一屏中能同时看到多幅(20幅)排好的图片,感兴趣的才打开浏览,那在这里展示缩略图是最好的,那这里缩略图就有一个排列和加载的问题,因为本身图片文件大小是不一致的,做成缩略图后图片大小也不一致,加载有快有慢,而且不希望按顺序挨个加载显示,最好的效果是打开一屏时,图片先用空位置占位,各个缩略图再独立加载,互不影响,加载完后自然显示
既然缩略图要异步展示,那在网络通讯上,发起图片请求再等待服务器返回的方式就达不到效果了,那我干脆在每个客户端也像服务端一样,也设置TCP服务,客户端只管向服务端请求图片而不等待返回,而服务端收到图片请求后,按照客户端提供的“客户端服务”信息,将图片数据发送到对应客户端所在TCP服务端口。总之客户端只管请求就可以了。
public void showGridData() { List<ImageDetailEntity> data = ImagePageBean.getInstance() .getCurrentQueryInfo(); jPanel.removeAll(); //jPanel是母板,所有缩略图在其上布局 int imgSize = data.size(); int rows = imgSize / cols; if (imgSize % cols > 0) { rows = rows + 1; } jPanel.setLayout(new GridLayout(rows, cols)); for (int i = 0; i < imgSize; i++) { ImageDetailEntity one = data.get(i); String id = one.getImgId(); String oldfileName = one.getFileName(); final JPanel pan = new JPanel(); ImageGridPanel ip = ImageGridPanel.createEmptyPanel(one, cellSize + 20); // 创建显示图片加载的面板 imagePanelMap.put(id, ip); pan.setMaximumSize(new Dimension(cellSize, cellSize + 20)); pan.add(ip); pan.setName(id); pan.setToolTipText(oldfileName); jPanel.add(pan); //将空的缩略图先占位 pan.setDoubleBuffered(true); //双缓冲提升显示效果 pan.addMouseListener(new MouseListener() { public void mouseClicked(MouseEvent me) { ....//鼠标事件,点击缩略图的响应事件,比如加菜单之类 } }); jPanel.add(pan); this.updateUI(); } jPanel.setVisible(true); }
public static ImageGridPanel createEmptyPanel(ImageDetailEntity idEntity,int panelSize) { ImageGridPanel panel = new ImageGridPanel(); panel.panelSize = panelSize + 20; Dimension size = new Dimension(panelSize, panelSize + 20); panel.setPreferredSize(size); String overviewName = idEntity.getOverviewName(); //将当前的占位图实例缓存,等文件下载完毕后再操作占位图实例,重刷占位图界面而显示图片 gridPanelMap.put(idEntity.getImgId(), panel); try { //此处开辟了一个线程去请求服务器上的图片,不会阻塞占位图的显示 ImagePanel.loadGridImage(idEntity.getImgId(), new File( DataConstants.ImageSaveDirectoryClient + overviewName), panelSize, panelSize); } catch (IOException e) { e.printStackTrace(); DataConstants.printErrorLog("创建空Panel出错:" + e.getMessage()); } return panel; }
//占位图每次updateUI都会触发该方法,这个方法很有意思,g.drawXXX可以渲染所有展示
public void paintComponent(Graphics g) {
if (img == null) {
if (infoString == null) {
g.drawString("图片加载中", 30, 30);
} else {
g.drawString(infoString, 30, 30);
}
return;
}
//以下是给缩略图画外框,使展现有立体感
g.setColor(Color.lightGray);
int tmpSize = Math.max(img.getWidth(null), img.getHeight(null))
+ padding * 2;
g.drawRect(1, 1, tmpSize - 1, tmpSize - 1); // 画外框
g.drawRect(startLeft - 2, startTop - 2, img.getWidth(null) + 4, img
.getHeight(null) + 4); // 画内框
g.drawLine(startLeft, startTop + img.getHeight(null) + 4, startLeft - 2
+ img.getWidth(null) + 6, startTop + img.getHeight(null) + 4);
g.drawLine(startLeft - 2 + img.getWidth(null) + 6, startTop, startLeft
- 2 + img.getWidth(null) + 6, startTop + img.getHeight(null)
+ 4);
//将图片渲染到panel
if (img != null) {
g.drawImage(img, startLeft, startTop, Color.RED, null);
} else {
g.setColor(Color.red);
g.drawString("无图像信息", startLeft + 100, startTop + 100);
}
//图片下方的标题展示
if (fileName != null && !"".equals(fileName)) {
g.setColor(Color.black);
int len1 = fileName.getBytes().length;
int len2 = fileName.length();
if (len1 != len2) {// 表示有中文【后注:当时这个判断中文的手法不太好】
if (len2 <= 15) {
int left = 8 + (150 - len2 * 5) / 2;
g.drawString(fileName, left, tmpSize + 15);
} else {
g.drawString(fileName.substring(0, 12) + "...", 8,
tmpSize + 15);
}
} else {
int left = 0;
if (len1 <= 30) {
left = 8 + (150 - len1 * 5) / 2;
g.drawString(fileName, left, tmpSize + 15);
} else {
g.drawString(fileName.substring(0, 27) + "...", 8,
tmpSize + 15);
}
}
}
}
这样一个简单的缩略图格点展现就完成了。
还有很多有趣的细节,在之后的博文会陆续分享,感兴趣的同学可以问我要源码,欢迎交流。
界面示例见附件