【Java实现】南京地铁导航系统的简单实现(三)—— 图形化界面的设计
关于如何存储信息、 最短路径算法的实现已经在之前章节说过,这里就不重复了,简单回顾一下实现内容。麻烦有需求的看官Tp到前一节:
【Java实现】南京地铁导航系统的简单实现(一)—— 存储站点信息_kksp993的博客-CSDN博客
【Java实现】南京地铁导航系统的简单实现(二)—— 最短路径算法的实现_kksp993的博客-CSDN博客
实现内容
以南京地铁运营示意图为模板,实现任意两个站点之间最优路径导航的规划与动态展示效果。具体模板图片以及要求如下:
1. 存储南京地铁线路站点信息。
2. 给定起点站和终点站,假设相邻站点路径长度相等,求路径最短的地铁乘坐方案;
3. 给定起点站和终点站,假设相邻站点路径长度相等,求换乘次数最少的地铁乘坐方案,若存在多条换乘次数相同的乘坐方案,则给出换乘次数最少且路径长度最短的乘坐方案。
4. 在实际应用中,相邻站点的距离并不相等,假设中转站地铁停留时间为T1,非中转站地铁停留时间为T2,地铁换乘一次的时间消耗为T3(不考虑等待地铁的时间),地铁平均速度为v,相邻站点的路径长度已知,试求:在给定起点站和终点站的情况下,求乘坐时间最短的地铁乘坐方案。
5. 设计可视化的查询界面,对以上内容进行动态化展示。
(注明:此例使用Swing包写java图形化界面)
窗体JFrame的设计
很多初学者(emm其实我也是初学者)会认为图形化界面很难做,然后在网上找到一些给了个JFrame生成了最基础窗体的程序然后只会改改宽高,对于监听、事件响应、布局管理器了解很少。这里想通过这个例子阐释这一部分的实现技巧。
首先说下原理,为了实现程序解耦,需要每个部件各司其职,对于我们这个项目而言,可以分为三个部分:窗口、地图组件、控制组件,基本上就可以按照功能简单分类。这样编程可以尽量减少维护运营的复杂程度,把大问题化成小问题而逐个击破,很方便。
(emmm,不要和我之前一样直接在JFrame上画图。。。)
实现功能:
(1)可以像高德地图/百度地图/腾讯地图/....完成基本的地图缩放、平移、选择等操作
(2)完成业务的可视化表示:
①用户选择站点,在下面红框中显示表示选为起始站/终点站
②根据右边按钮可以导航/清楚所选
③调用相关程序(上一节的程序)完成路径导航,获得导航路径
④通知地图模块绘制路线信息。
(3)其他特效功能。
先上代码,再进行解释:
package gui;import db.ParseDom4J;import javax.swing.*;
import java.awt.*;@SuppressWarnings("serial")
public class MGFrame extends JFrame {private static int frame_width = 640;private static int frame_height = 800;private static int frame_sX = (int) ((Toolkit.getDefaultToolkit().getScreenSize().getWidth() - frame_width) / 2);private static int frame_sY = (int) ((Toolkit.getDefaultToolkit().getScreenSize().getHeight() - frame_height) / 2);private static MapPanel mapPanel = new MapPanel(540, 540);private static ControlPanel controlPanel = new ControlPanel();public MGFrame() {setLocation(frame_sX, frame_sY);setSize(frame_width, frame_height);setTitle("MetroGuide");setResizable(false);setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);setVisible(true);setLayout(new FlowLayout(FlowLayout.CENTER, 5, 40));add(mapPanel);add(controlPanel);}public static void main(String[] args) throws Exception {ParseDom4J.main(args);JFrame mGFrame = new MGFrame();}public static ControlPanel getControlPanel() {return controlPanel;}public static MapPanel getMapPanel() {return mapPanel;}
}
继承自JFrame的类,GUI最好与核心包分开。不要写在一起,很混乱不易维护。
public class MGFrame extends JFrame {}
这一部分是表示窗体的宽高,(sX,sY)表示窗体左上角顶点的坐标。由于Java是以左上角屏幕点为(0,0),横向构成x轴,竖向构成y轴,以1像素为分度值,屏幕上的点坐标值均为非负数。下面的公式(其实正常拿个笔画一画就可以明白,把(sX,sY)设置为使得窗体上下左右留白对称即可)能够使得窗体居中。
private static int frame_width = 640; private static int frame_height = 800; private static int frame_sX = (int) ((Toolkit.getDefaultToolkit().getScreenSize().getWidth() - frame_width) / 2); private static int frame_sY = (int) ((Toolkit.getDefaultToolkit().getScreenSize().getHeight() - frame_height) / 2);
窗体类的构造函数。由于该类是窗体,一旦该类生成了,程序员就一定想使它以最优的形态展现在用户面前,因此把初始化部分写在构造函数里就好了。
这部分可写的代码其实很多,也很复杂,但是常用的就这些:
setLocation(frame_sX, frame_sY);设置左上角顶点的坐标值(这个之前已经已经写好了field值了,所以直接可以用(这样做可以方便程序更改窗体大小))。
setSize(frame_width, frame_height);设置宽高(其实这两步可以由setBounds(...)一步实现)
(如果你设置完没有反应,换成setPreferredSize(...),这个函数优先级会高一些)
setTitle("MetroGuide");设置窗体标题栏的名称,一般是应用名。
setResizable(false);使得窗体不能改变大小,什么意思?就是说你不能通过把鼠标放在窗口边界上,然后鼠标拖拽使得窗体的大小发生改变。这个是默认置true的,但是一般情况下改变大小不利于内部组件的大小设置,可能会有横向纵向拉伸,就会不好看。对于我这个不想让用户改变窗体大小的程序,置false。
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);这句你写上就好了。什么意思呢?写上这个是如果把窗体关了,就是点击右上角的红叉叉把窗口关了,会同时把程序也关了,否则如果不写的话,程序会还在运行。
setVisible(true);这句你写上就好了。什么意思?就是说设置窗口可见,默认是不可见的,也就是说,你不写,这个窗口你就看不见,也点不着。
setLayout(new FlowLayout(FlowLayout.CENTER, 5, 40));这个是设置了一个流式布局,流式布局听起来高大上难以理解,其实不然。流式布局关键在“流”,“流”译之为水流。夫水迂回而前进,遇礁石而折返。流式布局就好比水流,向着某一个方向摆放组件直到撞到该容器的容器壁,然后折返回来(如果是横着的,那么就好比你在word中打字,从左往右,到行尾(也就是容器壁),折返回下一行开头,那么横向的流式布局就是这样的布局规定)。
FlowLayout.CENTER每行中的组件居中,属于align属性。具体参见:
关于java中FlowLayout(流布局管理器)中的常量LEADING等问题_星月昭铭的博客-CSDN博客
5,40表示horizontal gap(hgap)与vertical gap(vgap)的值,hgap是横向组件间距为5像素,vgap表示行间距为40像素。
add(mapPanel); add(controlPanel);加入组件,这个时候是后面需要用的组件,都用add方法加入容器。
public MGFrame() {setLocation(frame_sX, frame_sY);setSize(frame_width, frame_height);setTitle("MetroGuide");setResizable(false);setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);setVisible(true);setLayout(new FlowLayout(FlowLayout.CENTER, 5, 40));add(mapPanel);add(controlPanel); }
主函数 首先执行业务逻辑,然后绘制UI界面,因为JFrame不点关它不关,所以这里不需要设置死循环之类的。
public static void main(String[] args) throws Exception {ParseDom4J.main(args);JFrame mGFrame = new MGFrame(); }
其他setget方法就不说了。
MapPanel设计
emmm,代码有点长,我先放上来,挑重点讲:
(ps.虽然我也明白,很多人见到代码就溜了....emmm,再看看?)
package gui;import core.LogicalPoint;
import core.Station;import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.HashMap;public class MapPanel extends JPanel {private static int blockwidth = 20;private static final int BST_BLOCK_WIDTH = 20;private static final int MAX_BLOCK_WIDTH = 60;private static final int MIN_BLOCK_WIDTH = 15;private int width;private int height;private static final Point BST_BASEPOINT = new Point(0, 0);private Point basePoint = new Point(0, 0);private static HashMap<Integer, Color> colors = new HashMap<>();private static boolean isShowing = false;private static boolean isViewPort = false;private static ArrayList<Station> path;private static int process = 0;public MapPanel(int width, int height) {setPreferredSize(new Dimension(width, height));setBackground(Color.white);setAllcolor();addMouseListener(new MouseListener() {@Overridepublic void mouseClicked(MouseEvent e) {endShowing();Point phyClickP = e.getPoint();LogicalPoint lgcClickP = getLogicalPoint(phyClickP);Station station = Station.getStationforAddr(lgcClickP);if (station == null) return;System.out.println(station);ControlPanel.selectStation(station);}@Overridepublic void mousePressed(MouseEvent e) {}@Overridepublic void mouseReleased(MouseEvent e) {}@Overridepublic void mouseEntered(MouseEvent e) {}@Overridepublic void mouseExited(MouseEvent e) {}});addMouseMotionListener(new MouseMotionListener() {private Point lastPoint;@Overridepublic void mouseDragged(MouseEvent e) {Point curPoint = e.getPoint();int dx = curPoint.x - lastPoint.x;int dy = curPoint.y - lastPoint.y;basePoint.x += dx;basePoint.y += dy;lastPoint = curPoint;repaint();}@Overridepublic void mouseMoved(MouseEvent e) {lastPoint = e.getPoint();}});addMouseWheelListener(new MouseWheelListener() {@Overridepublic void mouseWheelMoved(MouseWheelEvent e) {// 如果车轮旋转值为负,则表示向上旋转,而// 正值表示向下旋转if (e.getWheelRotation() < 0) {ModifyView(1, e.getPoint());} else if (e.getWheelRotation() > 0) {ModifyView(-1, e.getPoint());}}});}/*** 缩放屏幕窗口* @param dx 缩放力度,>0放大,<0缩小,dx值影响blockWidth大小* @param center 缩放中心*/private void ModifyView(int dx, Point center) {if (dx == 0) return;if (blockwidth > MAX_BLOCK_WIDTH && dx > 0) return;if (blockwidth < MIN_BLOCK_WIDTH && dx < 0) return;basePoint.x += dx / Math.abs(dx) * (basePoint.x - center.getX()) / blockwidth;basePoint.y += dx / Math.abs(dx) * (basePoint.y - center.getY()) / blockwidth;blockwidth += dx;repaint();}/*** 开启isViewPort标志,对视口进行平移缩放,得到最佳观测视角。* 当前设置的缩放倍率为2/3,级数求和能够缩放到指定位置,误差为1像素。*/private void viewPort() {isViewPort = true;int dx = blockwidth - BST_BLOCK_WIDTH;if (dx != 0)blockwidth -= dx / 1.5;if (!basePoint.equals(BST_BASEPOINT)) {basePoint.translate((int) ((BST_BASEPOINT.x - basePoint.x) / 1.5), (int) ((BST_BASEPOINT.y - basePoint.y) / 1.5));}if (Math.abs(dx) < 2 && Math.abs(basePoint.x - BST_BASEPOINT.x) < 2 && Math.abs(basePoint.y - BST_BASEPOINT.y) < 2) {isViewPort = false;}}/*** 动态显示path路线* @param path 需要显示的path路线*/public static void showPathNavigation(ArrayList<Station> path) {isShowing = true;MapPanel.path = path;MGFrame.getMapPanel().repaint();}private void setAllcolor() {colors.put(1, new Color(0x2897E6));colors.put(2, new Color(0xE82A2A));colors.put(3, new Color(0x15B612));colors.put(4, new Color(0xA513C0));colors.put(10, new Color(0xE6BE80));colors.put(-1, new Color(0x27D4C1));colors.put(-3, new Color(0xDA60CD));colors.put(-7, new Color(0xDD8699));colors.put(-8, new Color(0xFB631A));colors.put(-9, new Color(0xFFC500));}@Overridepublic void paint(Graphics g) {super.paint(g);if (isShowing)viewPort();paintMap(g);}private void paintMap(Graphics g) {Station[][] stationMap = Station.getStationsMap();for (int lineNum : Station.getLine_Map().keySet()) {drawStationLine(g, lineNum, !isShowing);if (isShowing) {drawShowingline(g, path);}}for (Station[] stations : stationMap) {for (Station station : stations) {drawStation(g, station);}}}/*** 绘制一个站点* @param g 画笔* @param station 站点*/private void drawStation(Graphics g, Station station) {if (station != null) {int smallOvalRadium = 2 + blockwidth / 18;Point phyPoint = getPhysicalPoint(station.getLoc());//中转站绘制白圈,其他绘制黑点if (!station.isTS()) {g.fillOval((int) phyPoint.getX() - smallOvalRadium, (int) phyPoint.getY() - smallOvalRadium, smallOvalRadium * 2, smallOvalRadium * 2);} else {g.setColor(Color.white);g.fillOval((int) phyPoint.getX() - smallOvalRadium, (int) phyPoint.getY() - smallOvalRadium, smallOvalRadium * 2, smallOvalRadium * 2);g.setColor(Color.black);g.drawOval((int) phyPoint.getX() - smallOvalRadium, (int) phyPoint.getY() - smallOvalRadium, smallOvalRadium * 2, smallOvalRadium * 2);}//判断上下左右,绘制站名if (station.getStationRight() == null)g.drawString(station.toString().substring(0, Math.min(blockwidth / 15, station.toString().length())), (int) phyPoint.getX() + 5,(int) phyPoint.getY() + 5);else if (!station.isOccpyUpLeft())g.drawString(station.toString().substring(0, Math.min(blockwidth / 15, station.toString().length())), (int) phyPoint.getX() - smallOvalRadium - 5,(int) phyPoint.getY() - 2 * smallOvalRadium);else if (station.getStationLeft() == null)g.drawString(station.toString().substring(0, Math.min(blockwidth / 15, station.toString().length())), (int) phyPoint.getX() - blockwidth + 5,(int) phyPoint.getY() + 5);else if (station.getStationDowm() == null)g.drawString(station.toString().substring(0, Math.min(blockwidth / 15, station.toString().length())), (int) phyPoint.getX() - smallOvalRadium - 5,(int) phyPoint.getY() + 2 * smallOvalRadium + 10);}}/*** 绘制线路* @param g 画笔* @param lineNum 线路编号* @param isColored 是否上色*/private void drawStationLine(Graphics g, int lineNum, boolean isColored) {ArrayList<Station> line = Station.getLine_Map().get(lineNum);Point curPoint, lastPoint = getPhysicalPoint(line.get(0).getLoc());g.setColor(isColored ? colors.get(lineNum) : Color.gray);drawEdge(g, line, lastPoint);}/*** 绘制选中路线* @param g 画笔* @param line 选中的线路*/private void drawShowingline(Graphics g, ArrayList<Station> line) {Point curPoint, lastPoint = getPhysicalPoint(line.get(0).getLoc());drawProcessEdge(g, line, lastPoint, 1.0 * process / 100);if (process < 99 && !isViewPort) process++;try {Thread.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}repaint();}/*** 绘制一条边* @param g 画笔* @param line 线路* @param lastPoint 路由节点,用于判断是几号线*/private void drawEdge(Graphics g, ArrayList<Station> line, Point lastPoint) {drawProcessEdge(g, line, lastPoint, 1);}/*** 绘制一条渐进的线* @param g 画笔* @param line 绘制数组* @param lastPoint 上一路由节点* @param process 进度条*/private void drawProcessEdge(Graphics g, ArrayList<Station> line, Point lastPoint, double process) {Point curPoint;((Graphics2D) g).setStroke(new BasicStroke(blockwidth / 6, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));for (int i = 1; i < line.size() * process && line.get(i) != null; i++) {if (isShowing & process < 1) {g.setColor(colors.get(line.get(i).commonLineNum(line.get(i - 1))));((Graphics2D) g).setStroke(new BasicStroke(blockwidth / 4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));}curPoint = getPhysicalPoint(line.get(i).getLoc());g.drawLine(lastPoint.x, lastPoint.y, curPoint.x, curPoint.y);lastPoint = curPoint;}((Graphics2D) g).setStroke(new BasicStroke(1.0f));g.setColor(Color.BLACK);}/*** 结束当前高亮显示*/public void endShowing() {if (isShowing) {isShowing = false;process = 0;}}/*** 获得具体画在panel上的点* @param lgPoint 站点矩阵的逻辑点* @return 具体画在panel上的点*/private Point getPhysicalPoint(LogicalPoint lgPoint) {return new Point((int) (lgPoint.getX() * blockwidth + basePoint.getX()),(int) (lgPoint.getY() * blockwidth + basePoint.getY()));}/*** 获得panel上相应点对应的逻辑点* @param point 具体画在panel上的点* @return 站点矩阵对应的逻辑点*/private LogicalPoint getLogicalPoint(Point point) {return new LogicalPoint((int) Math.floor((point.getX() - basePoint.getX()) / blockwidth + 0.5),(int) Math.floor((point.getY() - basePoint.getY()) / blockwidth + 0.5));}
}
首先讲一下这种地图绘制的原理。首先一个组件在窗口上显示,需要有明确的参考系,对于参考系默认是该组件左上角点为(0,0)。但是,很显然,这个参考点动不了,不够灵活。例如:中国地图,如果我想聚焦北京市,那么我应当只显示北京市的地图,而不会把从新疆一直到北京都显示出来。因为你的参考系是死的,也就是(0,0)动不了,如果非要这样调整就需要调整整个图片各点坐标。(北京以西的地方x坐标都是负的,对于事件监听需要重新定位比较复杂)。
这里采用的是基于参考点的坐标系,也就是说所有图形绘制基于的是参考点BasePoint,那么问题就简单了:(这个参考点相对于(0,0)而言,是可以改变的可以变换的)
(1)对于题目中棋盘状的点阵形式,可以使用(m,n)一一映射到具体的参考位置上:
其中表示棋盘上的逻辑点位置,比如迈皋桥是,对应初始状态下的物理坐标为如果为的话;同样,点击时,只要x,y都在该点左右1/2间距以内都算点到这个格点的,因此四舍五入需要加个0.5。这样可以一一对应了,后面就不需要再为转化而烦恼了。
(2)对于用户平移类操作,可以直接在上修改,绘制时所有点自动向对齐:
(3)对于用户缩放类操作,需要基于鼠标位置和滚轮进度对和blockwidth进行修正:
当用户以鼠标某一位置缩放地图时,用户实际想要看清楚鼠标所指之处——以鼠标为中心进行缩放。这样会造成基准点的移动,需要以中心点为参考点更改基准点的坐标。
对于鼠标在图像C点,基准点在,由于缩放(以blockwidth自增为例),的
长度增加了原来的1/blockwidth倍(例如:一开始是blockwidth是20,那么当所有间距扩大一格时,变为原来的1.05倍),但相对方向不变,因此可以以此更新基准点坐标。
具体而言,新的基准点满足如下公式:
其中,表示鼠标位置,表示基准点位置,表示缩放完成后的。(emmm,上面公式好像打成Pb了,和bp一个意思。)
监听相应代码书写如下:
这个交互是通过监听实现的,采用匿名内部类的形式重写相关抽象方法,实现相应功能:
(1)添加鼠标点击
addMouseListener(new MouseListener() {@Overridepublic void mouseClicked(MouseEvent e) {endShowing();Point phyClickP = e.getPoint();LogicalPoint lgcClickP = getLogicalPoint(phyClickP);Station station = Station.getStationforAddr(lgcClickP);if (station == null) return;System.out.println(station);ControlPanel.selectStation(station);}@Overridepublic void mousePressed(MouseEvent e) {}@Overridepublic void mouseReleased(MouseEvent e) {}@Overridepublic void mouseEntered(MouseEvent e) {}@Overridepublic void mouseExited(MouseEvent e) {} });
由于没法不写其他4个方法,所以就写个空方法。对于点击,我们的操作可以获得该站点。由于之前说过,我们是以名称相同为站点的唯一标识符,因此通过上述定位方法,获取站点,看是哪一站。之后就是相应的处理了(这里点完交给控制模块处理(控制模块用点击的信息作为导航起点/终点,交由上一节的最短路线程序寻找路线,再在此地图上绘制出来,即可完成人机交互))
(2)添加鼠标拖拽监听
addMouseMotionListener(new MouseMotionListener() {private Point lastPoint;@Overridepublic void mouseDragged(MouseEvent e) {Point curPoint = e.getPoint();int dx = curPoint.x - lastPoint.x;int dy = curPoint.y - lastPoint.y;basePoint.x += dx;basePoint.y += dy;lastPoint = curPoint;repaint();}@Overridepublic void mouseMoved(MouseEvent e) {lastPoint = e.getPoint();} });
这一段两个重写方法分别对应如下两个鼠标操作:
①: mouseDragged鼠标拖拽:当你摁下鼠标,并拽动的时候,它会每隔一会采样一次鼠标的位置,执行该方法。
②: mouseMoved 鼠标移动:与上面相对应,如果你鼠标移动,但你没有摁下,那么会执行这个方法;当鼠标摁下后,就不会执行此方法。
那么拖拽功能实现如下:
拖拽的时候需要实时更新,所以需要记录上一帧鼠标的位置。两者的偏移量表示用户想要把地图拽到鼠标当前位置,由于地图发生的变化是线性的,不会发生畸变,因此对于地图上每个点都会跟着鼠标移动到目标位置,移动量就是鼠标偏移量。那么很显然基准点也是地图上的点,所以只需要将基准点加上这个便宜即可。
对于第一下拖拽,由于lastPoint没有实时更新,所以点一下就会将地图瞬间闪到很远的地方,这种情况需要解决。一般的方法是第一帧舍弃,但不好编写。这里在mouseMoved函数中一直默认更新当前点,由于程序启动瞬间用户不可能直接拖拽(人毕竟是人,有反应时间的),所以相当于lastPoint一直是更新的了,不会有bug。
注意repaint()
(3)添加鼠标滚轮监听
addMouseWheelListener(new MouseWheelListener() {@Overridepublic void mouseWheelMoved(MouseWheelEvent e) {// 如果车轮旋转值为负,则表示向上旋转,而// 正值表示向下旋转if (e.getWheelRotation() < 0) {ModifyView(1, e.getPoint());} else if (e.getWheelRotation() > 0) {ModifyView(-1, e.getPoint());}} });
/*** 缩放屏幕窗口* @param dx 缩放力度,>0放大,<0缩小,dx值影响blockWidth大小* @param center 缩放中心*/ private void ModifyView(int dx, Point center) {if (dx == 0) return;if (blockwidth > MAX_BLOCK_WIDTH && dx > 0) return;if (blockwidth < MIN_BLOCK_WIDTH && dx < 0) return;basePoint.x += dx / Math.abs(dx) * (basePoint.x - center.getX()) / blockwidth;basePoint.y += dx / Math.abs(dx) * (basePoint.y - center.getY()) / blockwidth;blockwidth += dx;repaint(); }
这里由于放大缩小基本上是同构的,因此用一个方法就可以同意求解。
mouseWheelMoved:当滚轮运动时,响应事件。
e.getWheelRotation:如果车轮旋转值为负,则表示向上旋转,而正值表示向下旋转
e.getScrollAmount():滚轮滚动值。这个由操作系统决定,一般没改过就是3.
缩放函数已经在上面讲过了,这就不说了。
注意repaint()
绘图相应代码书写如下:
@Override public void paint(Graphics g) {super.paint(g);if (isShowing)viewPort();paintMap(g); }
绘图主要在这个方法里写。这个函数不要把整个所有代码全写进去,因为这样你很快就会乱掉了,应该写成一个个子函数,然后调用他们,这样能够看到画图的所有流水过程。
一般绘图有以下一些方法供参考:
g.setColor(Color.white);
设置画笔颜色为白色,画笔属性会影响到整个绘制过程,比如画了个线,线会变颜色变粗细!
(用之前记得保存原来的画笔,用完再把它洗掉,还原成原来的样子,否则你其他地方也用了这样的笔触,就很烦)
((Graphics2D) g).setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
设置粗细 3.0f,使用BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND参数。这个基本上就改改粗细就好了,就是那个3.0f(具体看链接)
g.drawLine(lastPoint.x, lastPoint.y, curPoint.x, curPoint.y);
划线
g.fillOval(100,100,40,40);g.drawOval(100,100,40,40);
fill族和draw族分别是画全涂色和边缘涂色的图形。
前面的是图形(外接正方形)左上角的点,后面的是宽高。
相关参考:
BasicStroke的用法_李腾飞的专栏-CSDN博客_basicstroke
其他的程序设计就是你自己想画什么画什么了/^.^/
ControlPanel设计
这部分就比较简单了,没什么特别的,完全根据自己的想法一个个放置就好了。
package gui;import core.PathHelper;
import core.Station;import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;public class ControlPanel extends JPanel {private static Station originStation;private static Station terminal;private static JLabel osLabel;private static JLabel tsLabel;private JButton ClearBtn;private JButton NaviBtn;private JButton lstTsBtn;private JButton weightBtn;public ControlPanel() {setPreferredSize(new Dimension(600, 130));setBackground(Color.red);setLayout(new FlowLayout(FlowLayout.LEADING, 25, 15));osLabel = initLabel("", 180, 45);osLabel.setBorder(BorderFactory.createLineBorder(Color.black));tsLabel = initLabel("", 180, 45);tsLabel.setBorder(BorderFactory.createLineBorder(Color.black));ClearBtn = initButton("清除", 100, 45);NaviBtn = initButton("站点少", 100, 45);lstTsBtn = initButton("换乘少", 100, 45);weightBtn = initButton("时间短", 100, 45);add(initLabel("起始站:", 90, 45));add(osLabel);add(NaviBtn);add(lstTsBtn);add(initLabel("终点站:", 90, 45));add(tsLabel);add(weightBtn);add(ClearBtn);ClearBtn.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {MGFrame.getMapPanel().endShowing();clearInfo();repaint();}});NaviBtn.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {NaviEventPerformed(ClearBtn, 1);}});lstTsBtn.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {NaviEventPerformed(ClearBtn, 100);}});weightBtn.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {NaviEventPerformed(ClearBtn, 2);}});}/*** 导航开始* @param ClearBtn 清楚按钮对象,方便运行玩清除* @param TS_Non 导航功能参数*/private static void NaviEventPerformed(JButton ClearBtn, int TS_Non) {MGFrame.getMapPanel().endShowing();if (originStation == null || terminal == null || originStation.equals(terminal)) {ClearBtn.doClick();return;}PathHelper.PathNavigation(originStation, terminal, TS_Non);ArrayList<Station> path = PathHelper.getPath();int showConfirmDialog = JOptionPane.showConfirmDialog(null, "导航已完成:共" + path.size() + "站\n是否查看导航", "MetroGuide为您导航", JOptionPane.YES_NO_OPTION);//当我们点击"是",返回值为0;//当我们点击"否",返回值为1;//当我们点击"×",关闭了选择框,此时返回值为-1.if (showConfirmDialog == 0)MapPanel.showPathNavigation(path);elseClearBtn.doClick();}/*** 提供MapPanel 选取站点的函数* @param station 被选择的站,第一次选入的是起始站,第二次选入的是终点站* ,第三次则会认为是误操作,清楚所有站。*/public static void selectStation(Station station) {if (originStation == null) {originStation = station;osLabel.setText(station.toString());} else if (terminal == null) {terminal = station;tsLabel.setText(station.toString());} else {MGFrame.getControlPanel().clearInfo();}}/*** 生成一个JLabel* @param s 标题* @param width 宽* @param height 高* @return JLabel*/private JLabel initLabel(String s, int width, int height) {JLabel label = new JLabel(s, JLabel.CENTER);label.setBackground(Color.red);label.setPreferredSize(new Dimension(width, height));label.setFont(new Font("微软雅黑", Font.BOLD, 24));return label;}/*** 生成一个Jutton* @param s 标题* @param width 宽* @param height 高* @return JButton*/private JButton initButton(String s, int width, int height) {JButton button = new JButton(s);button.setPreferredSize(new Dimension(width, height));button.setFont(new java.awt.Font("华文行楷", 1, 20));button.setBackground(Color.red);return button;}@Overridepublic void paint(Graphics g) {super.paint(g);}public void clearInfo() {originStation = null;terminal = null;osLabel.setText("");tsLabel.setText("");}}
主要说说这部分:
ClearBtn.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {MGFrame.getMapPanel().endShowing();clearInfo();repaint();} }); NaviBtn.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {NaviEventPerformed(ClearBtn, 1);} }); lstTsBtn.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {NaviEventPerformed(ClearBtn, 100);} }); weightBtn.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {NaviEventPerformed(ClearBtn, 2);} });
这边是每个摁键给它装上自己的监听,对应摁下之后会执行actionPerformed函数
/*** 导航开始* @param ClearBtn 清楚按钮对象,方便运行玩清除* @param TS_Non 导航功能参数*/ private static void NaviEventPerformed(JButton ClearBtn, int TS_Non) {MGFrame.getMapPanel().endShowing();if (originStation == null || terminal == null || originStation.equals(terminal)) {ClearBtn.doClick();return;}PathHelper.PathNavigation(originStation, terminal, TS_Non);ArrayList<Station> path = PathHelper.getPath();int showConfirmDialog = JOptionPane.showConfirmDialog(null, "导航已完成:共" + path.size() + "站\n是否查看导航", "MetroGuide为您导航", JOptionPane.YES_NO_OPTION);//当我们点击"是",返回值为0;//当我们点击"否",返回值为1;//当我们点击"×",关闭了选择框,此时返回值为-1.if (showConfirmDialog == 0)MapPanel.showPathNavigation(path);elseClearBtn.doClick(); }
这个部分抽出来写,否则很多代码是一样的。首先进行一些简单的提升健壮性的判断。是否正在导航?(这里直接给它摁了,你没结束我也强制结束)是否起点终点都有且不相同?都符合,我们进行导航——调用章节(二)提供的接口(emmm不是interface),得到最佳路径,弹出如下人性化对话框:
点击是了,我们就提供导航,在之前地图中动态显示路线,否则我们就不管它。无论如何,都需要清空该次导航信息,方便下次导航。(如果有心也可以做个保存历史的,存在某个info文件里,每次读就好了)
这里的动态效果做的一般,就不说了。准确来说只要给用户慢慢显示路线延伸方向就好。
最后一些测试环节: (左中右依次为站点少、换乘少、时间短)
以下为控制台打印信息:
(注:其中dp_cost分别表示站点数量、换乘加权得分、用时分钟,因数据不足,模型假设地铁配速60km/h,上下站1分钟,中转站等待3分钟,可能与实际有所偏差)
仙林中心
步月路
------------start--------------1.0
仙林中心= [ 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 ]
步月路= [ 油坊桥 , 南京南站 ]
dp_tags= [ 仙林中心 , 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 , 油坊桥 , 南京南站 , 步月路 ]
dp_path= [ 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 新街口 , 大行宫 , 油坊桥 ]
dp_cost= [ 0.0 , 3.0 , 11.0 , 12.0 , 20.0 , 22.0 , 21.0 , 19.0 , 29.0 ]
path= [ 仙林中心 , 学则路 , 仙鹤门 , 金马路 , 马群 , 钟灵街 , 孝陵卫 , 下马坊 , 苜蓿园 , 明故宫 , 西安门 , 大行宫 , 新街口 , 张府园 , 三山街 , 中华门 , 安德门 , 小行 , 中胜 , 元通 , 雨润大街 , 油坊桥 , 中和街 , 黄河路 , 天河路 , 新梗街 , 天保路 ,生态科技园 , 滨江村 , 步月路 ]
-------------end---------------
------------start--------------10.0
仙林中心= [ 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 ]
步月路= [ 油坊桥 , 南京南站 ]
dp_tags= [ 仙林中心 , 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 , 油坊桥 , 南京南站 , 步月路 ]
dp_path= [ 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 油坊桥 , 大行宫 , 油坊桥 ]
dp_cost= [ 0.0 , 21.0 , 29.0 , 30.0 , 38.0 , 40.0 , 40.0 , 55.0 , 48.0 ]
path= [ 仙林中心 , 学则路 , 仙鹤门 , 金马路 , 马群 , 钟灵街 , 孝陵卫 , 下马坊 , 苜蓿园 , 明故宫 , 西安门 , 大行宫 , 新街口 , 上海路 , 汉中门 , 莫愁湖 , 云锦路 ,集庆门大街 , 兴隆大街 , 奥体东 , 元通 , 雨润大街 , 油坊桥 , 中和街 , 黄河路 , 天河路 , 新梗街 , 天保路 ,生态科技园 , 滨江村 , 步月路 ]
-------------end---------------
------------start--------------2.0
仙林中心= [ 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 ]
步月路= [ 油坊桥 , 南京南站 ]
dp_tags= [ 仙林中心 , 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 , 油坊桥 , 南京南站 , 步月路 ]
dp_path= [ 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 金马路 , 大行宫 , 南京南站 ]
dp_cost= [ 0.0 , 9.2 , 27.9 , 32.2 , 55.7 , 63.5 , 61.1 , 46.8 , 89.0 ]
path= [ 仙林中心 , 学则路 , 仙鹤门 , 金马路 , 马群 , 钟灵街 , 孝陵卫 , 下马坊 , 苜蓿园 , 明故宫 , 西安门 , 大行宫 , 常府街 , 夫子庙 , 武定门 , 雨花门 , 卡子门 , 大明路 , 明发广场 , 南京南站 , 景明佳园 ,铁心桥大街 , 春江新城 , 华新路 , 油坊桥 , 中和街 , 黄河路 , 天河路 , 新梗街 , 天保路 ,生态科技园 , 滨江村 , 步月路 ]
-------------end---------------
以下为控制台打印信息:
仙林中心
柳州东路
------------start--------------1.0
仙林中心= [ 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 ]
柳州东路= [ 泰冯路 , 南京站 , 鸡鸣寺 , 大行宫 , 南京南站 ]
dp_tags= [ 仙林中心 , 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 , 泰冯路 , 南京站 , 鸡鸣寺 , 大行宫 , 南京南站 , 柳州东路 ]
dp_path= [ 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 金马路 , 金马路 , 金马路 , 大行宫 , 大行宫 , 鸡鸣寺 ]
dp_cost= [ 0.0 , 3.0 , 11.0 , 12.0 , 20.0 , 22.0 , 18.0 , 12.0 , 10.0 , 11.0 , 19.0 , 16.0 ]
path= [ 仙林中心 , 学则路 , 仙鹤门 , 金马路 ,苏宁总部·徐庄 , 聚宝山 , 王家湾 , 蒋王庙 , 岗子村 , 九华山 , 鸡鸣寺 , 新庄 , 南京站 , 小市 , 五塘广场 , 上元门 , 柳州东路 ]
-------------end---------------
------------start--------------10.0
仙林中心= [ 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 ]
柳州东路= [ 泰冯路 , 南京站 , 鸡鸣寺 , 大行宫 , 南京南站 ]
dp_tags= [ 仙林中心 , 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 , 泰冯路 , 南京站 , 鸡鸣寺 , 大行宫 , 南京南站 , 柳州东路 ]
dp_path= [ 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 大行宫 , 大行宫 , 金马路 , 大行宫 , 大行宫 , 大行宫 ]
dp_cost= [ 0.0 , 21.0 , 29.0 , 30.0 , 38.0 , 40.0 , 57.0 , 51.0 , 46.0 , 29.0 , 55.0 , 37.0 ]
path= [ 仙林中心 , 学则路 , 仙鹤门 , 金马路 , 马群 , 钟灵街 , 孝陵卫 , 下马坊 , 苜蓿园 , 明故宫 , 西安门 , 大行宫 , 浮桥 , 鸡鸣寺 , 新庄 , 南京站 , 小市 , 五塘广场 , 上元门 , 柳州东路 ]
-------------end---------------
------------start--------------2.0
仙林中心= [ 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 ]
柳州东路= [ 泰冯路 , 南京站 , 鸡鸣寺 , 大行宫 , 南京南站 ]
dp_tags= [ 仙林中心 , 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 , 泰冯路 , 南京站 , 鸡鸣寺 , 大行宫 , 南京南站 , 柳州东路 ]
dp_path= [ 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 金马路 , 金马路 , 金马路 , 大行宫 , 大行宫 , 鸡鸣寺 ]
dp_cost= [ 0.0 , 9.2 , 27.9 , 32.2 , 55.7 , 63.5 , 57.2 , 40.3 , 31.4 , 27.9 , 46.8 , 52.0 ]
path= [ 仙林中心 , 学则路 , 仙鹤门 , 金马路 ,苏宁总部·徐庄 , 聚宝山 , 王家湾 , 蒋王庙 , 岗子村 , 九华山 , 鸡鸣寺 , 新庄 , 南京站 , 小市 , 五塘广场 , 上元门 , 柳州东路 ]
-------------end---------------
这个路段高德地图、百度地图给出的是2->3的换乘路径,实测2->3->4更好(10分钟左右的省时间)。测试的地图软件只有腾讯地图给出了2->4->3的换乘路线,用时约为54min,与我们的52min高度符合。因此我信任了所谓的腾讯地图,把其他两家删掉了!删掉了!!
好了,以上就是这一小节讲解的简单GUI图形化界面制作。感谢友友们一键三连哦!希望这三节的讲解能够帮助到你!
【Java实现】南京地铁导航系统的简单实现(三)—— 图形化界面的设计相关推荐
- java网络编程作业基于UDP简单聊天窗口,图形化界面,包含客户端和服务端
//郑州轻工业大学 //题号:实验四 第二题 //题目:使用基于UDP的网络编程方法,完成客户端和服务器间的聊天功能.要求图形界面. java网络编程作业 基于UDP简单聊天窗口,图形化界面,包含客户 ...
- MFC入门(一)-- 第一个简单的windows图形化界面小程序(打开计算器,记事本,查IP)
/序 大约三年前,学过一些简单的编程语言之后其实一直挺苦恼于所写的程序总是拘泥于用的编译器,脱离了编译环境基本没运行的可行性,故而写一个在任意windows电脑下都能运行的小软件便成为了一块心病. 大 ...
- Java图形化界面设计——容器(JFrame)
Java图形化界面设计--容器(JFrame) 程序是为了方便用户使用的,因此实现图形化界面的程序编写是所有编程语言发展的必然趋势,在命令提示符下运行的程序可以让我们了解java程序的基本知识体系结构 ...
- java之图形化界面(GUI)
一.概述 用户与计算机进行交换的方式有两种: GLI:Command lin User Interface(命令行用户接口),也就是常见的dos窗口,它需要记住一些常用的命令,操作不直观方便. GUI ...
- JAVA Swing 图形化界面编程
JAVA Swing 图形化界面编程 目录 1.组件 1.1 基本组件 1.2. 组件边框 1.3. JToolBar 工具条 1.4 JColorChooser 颜色选择器 1.5 JFileCho ...
- # java swing,awt图形化界面代码案例合集
文章目录 java awt,swing图形化界面代码案例合集 java awt,swing图形化界面代码案例合集 package Demo1; import java.awt.*; public cl ...
- java 网格布局管理器,Java图形化界面设计——布局管理器之GridLayout(网格布局)...
网格布局特点: l 使容器中的各组件呈M行×N列的网格状分布. l 网格每列宽度相同,等于容器的宽度除以网格的列数. l 网格每行高度相同,等于容器的高度除以网格的行数. l 各组件的排列方式 ...
- Java图形化界面编程之——AWT
目录 1.AWT简介 2.AWT继承体系 3.Container容器 3.1.Container继承体系 3.2.常用API 3.2.1.Component的常用方法 3.2.2.Container的 ...
- JAVA图形化界面计算器
优秀的代码是它自己最好的文档.当你考虑要添加一个注释时,问问自己,"如何能改进这段代码,以让它不需要注释 我是一名在校大学生,这学期刚刚开JAVA课,老师上周留作业让做个计算器,最近一直在搞 ...
- Java图形化界面编程
Java图形化界面编程(使用AWT) 文章目录 内容概述 容器Container Window Panel ScrollPane Box 布局管理器 FlowLayout ...
最新文章
- input框取消光标颜色手机端不生效
- vivado----fpga硬件调试 (二)----mark_debug
- 圆柱属于能滚动的物体吗_中班科学活动教案:滚动的物体教案(附教学反思)
- labelme标注文件转coco json,coco json转yolo txt格式,coco json转xml, labelme标注文件转分割,boxes转labelme json
- rpm安装mysql5.7.16_【CentOS 6.6 RPM方式安装MySQL 5.7.16 】
- PHP 图像编辑GD库的使用以及图像的压缩
- 《Effective Python》笔记
- windows平台vs2010编译64位libiconv与libxml2
- Excel合并单元格中间插入斜杠和数字保留一位小数
- 金山毒霸--血淋淋的教训
- 枸杞最适合用来消除疲劳
- 33幅精美的拿铁图案摄影作品欣赏
- 我在CSDN和Unity有个约会
- 2022-2028年全球与中国医用级AC-DC电源行业竞争格局与投资战略研究
- 技术的发展与互联网的发展
- 排列计算公式,公式含义
- 学习python,北京尚学堂,第07课到第30课的个人的总结
- 电脑识别不到硬盘的问题
- AMD电脑装完Winsows10后开机蓝屏,报错代码:cdmsnroot_s.sys
- 数学建模 - 汽车行驶工况构建(2019年中国研究生数学建模竞赛D题)
热门文章
- 计算机专业考研复试个人简介ppt,蓝色简约考研复试个人简历通用ppt.pptx
- Java题目:一个整数,它加上100后是一个完全平方数,再加上168又是一个完全平方数,请问该数是多少?
- 2018最新圣思园JavaSE实地培训系列视频教程
- 安卓简单的通用文本编辑器介绍
- 【线性代数】矩阵分解(Matrix Factorization)笔记:非负矩阵分解(实践)
- 如何使用PAUP4、MrBayes、TNT构建系统发育树
- 密码只靠大脑记好累,有没有试过用群晖NAS来记?
- 现代雷达系统分析与设计---动目标检测(MTD)
- NLP实战 | BERT文本分类及其魔改(附代码)
- Android修行手册-EditText属性以及光标和小键盘控制