`
javatar
  • 浏览: 1701093 次
  • 性别: Icon_minigender_1
  • 来自: 杭州699号
社区版块
存档分类
最新评论

GUI客户端Task设计

阅读更多
主流的GUI库都是采用单线程,不管是Java的还是C++的,Java里的Swing/AWT和JFace/SWT当然都是,
因MVC的盛行,也就是观察者模式,或者说事件通知方式,如果GUI设计成多线程,开发人员必须小心翼翼的开发,稍不留神就会出现死锁或死循环,非常难用,开发人员基本上会疯掉,
据说AWT前期也想发展成多线程GUI库,这样界面更快,最终因易用性而放弃。

OK,不扯远了,即然GUI是单线程库,也就是所有的GUI操作都是在同一个GUI线程上操作的,并发操作时,有个队列进行同步排队。
在Swing里面这个特殊的线程就是著名的EDT(EventDispatchingThread),所有的GUI的变更以及相互间的事件通知都是通过这个线程进行分发的,
Swing有个不好的容错性设计,就像浏览器都支持不正规的HTML,导致写HTML的人越来越不注意正确的写法,
Swing允许你在非GUI线程直接调用GUI控件,在其内部会检查当前线程是否为EDT线程,如果不是,会自动分发到EDT队列,
这就可能使开发人员不关注EDT线程,从而导致更多的错误写法,最后就是大家都抱怨Swing写的GUI很卡,
比如:
public static void main(String[] args) {
	// 在主线程直接调用UI
	JFrame frame = new JFrame();
	frame.show();
}

正确的写法是:
public static void main(String[] args) {
	// 如果不需要等待GUI执行完成,可以用SwingUtilities.invokeLater
	// 也可以用EventQueue.invokeAndWait和EventQueue.invokeLater
	SwingUtilities.invokeAndWait(new Runnable() {
		public void run() {
			// 在EDT线程调用UI
			JFrame frame = new JFrame();
			frame.show();
		}
	});
}

而Eclipse用的JFace/SWT,当非GUI线程调用UI控件时,直接报错,虽然开始会觉得麻烦,但却减少了更多的问题,
强制你必需用下面的方式调用:
// syncExec等价于Swing的invokeAndWait
// 或者asyncExec等价于Swing的invokeLater
Display.getDefault().syncExec(new Runnable() {
	public void run() {
		// GUI有关的设置
	}
});


上面从非UI线程调UI的问题可能不算太严重,因为Swing内部有做转发,
但反过来,在UI线程内做太多事,就很严重了,比如:
component.addActionListener(new ActionListener() {
	public void actionPerformed(ActionEvent e) {
		// 执行很长时间的任务,比如远程调用,大数据较验等 ...
	}
});

GUI进程就会被阻塞,整个Swing窗口就卡住了,用户如果这时点一下界面,会发现界面不响应。
这也就是我们今天要解决的问题,在JDK1.6之前版本,
Swing是没有提供工具类的,你可以自己启一个线程或线程池操作非UI的逻辑,如:
component.addActionListener(new ActionListener() {
	public void actionPerformed(ActionEvent e) {
		new Thread(new Runnable() {
			public void run() {
				// 执行很长时间的任务,比如远程调用,大数据较验等 ...
			}
		}).start();
	}
});

或者用线程池:
component.addActionListener(new ActionListener() {
	public void actionPerformed(ActionEvent e) {
		threadPool.execute(new Runnable {
			public void run() {
				// 执行很长时间的任务,比如远程调用,大数据较验等 ...
			}
		});
	}
});

这样做的问题在于,如果我执行完后,要在界面上展示执行完的结果,
怎么办呢?当然最直接的是:
component.addActionListener(new ActionListener() {
	public void actionPerformed(ActionEvent e) {
		threadPool.execute(new Runnable() {
			public void run() {
				// 执行很长时间的任务,比如远程调用,大数据较验等 ...
				// final 结果
				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						// 在UI上显示结果
					}
				});
			}
		});
	}
});

这样的代码看的头有点晕,并且还是有几个问题没解决:
1. 任务是凌散的,没有统一管理,如果要做管理面板怎么处理?
2. 任务如何取消,万一有慢到一年都执行不完的任务,想取消怎么办?
3. 没有统一的出错处理?出错了每个地方都要重复处理。
5. 任务的进度,如果要做进度条怎么处理?
6. 任务的执行过程没有事件通知,如果我想监听任务的变化,怎么处理?

针对这些问题,JDK1.6终于加了一个类来处理,叫SwingWorker,看一下它的API:
public abstract class SwingWorker<T, V> implements RunnableFuture<T> {

	// Runnable
	void run();
	
	// Future<T>
	void cancel(boolean);
	boolean isCancelled();
	boolean isDone();
	T get();
	T get(long, TimeUnit);
	
	// SwingWorker<T, V>
	T doInBackground() throws Exception;
	void publish(V...);
	void process(List<V>);
	void done();
	void execute();
	void setProgress(int);
	int getProgress();
	StateValue getState();
	void addPropertyChangeListener(PropertyChangeListener);
	void removePropertyChangeListener(PropertyChangeListener);
	void firePropertyChange(String, Object, Object);
	PropertyChangeSupport getPropertyChangeSupport();
}

其问题,主要有:
1. 如果不看文档,你很难猜到它的用法,这个类集实体域,会话域,服务域于一身,非常复杂,即表示任务本身(实体域),也包含执行过程的状态(会话域),同时也具有主动执行能力(服务域)。
2. 签名上的两个泛型,第一个是总任务返回结果的类型,第二个是子任务的返回结果,然而大部分时候用不上子任务,却必需声明两个泛型。
3. 里面的潜规则也不少,比如:doInBackground()的返回值是给done()方法用的,而且要通过get()方法获取到返回值。
4. 没有Title, Message等描述信息。
5. 这个类和Thread类一样,只能执行一次,多次执行是无效的,因为其有状态,所以这样做是正确的。
它的用法如下:
new SwingWorker<List<User>, Void> () { // 没有子任务,第二个泛型声明为Void
	// 独立的线程池执行doInBackground方法,执行完后,结果放在get方法的Future引用中
	protected List<User> doInBackground() throws Exception {
		return remoteService.findUsers();
	}
	// 在EDT线程执行done方法,所有与GUI的相关操作都应放在done方法中处理
	protected done() {
		try {
			// 通过get获取doInBackground返回的结果
			List<User> users = get();
			// 显示到UI
			tableModel.addAll(users);
		} catch (Exception) {
			// 捕获doInBackground抛出的异常,由get方法抛出
		}
	}
}.execute(); // 直接执行,内部有封装线程池和FutureTask

JSR296(Swing Application Framework)对上面的问题做了一些修补,
定义了一个Task类继承于SwingWoker,但不彻底,而且JSR296没有定稿,现阶段也不能用,

基于这种现状,我们需要扩展自己的Task方案,
接口使用如下:
TaskExecutor.execute(new TaskSupport() {
	@TaskTitle("获取用户列表任务")
	public void execute(TaskContext context) {
		context.put("users", remoteService.findUsers());
	}
	public void succeeded(TaskEvent event) {
		List<User> users = event.getTaskContext().get("users");
		tableModel.addAll(users);
	}
});

1. TaskSupport同时实现Task接口和TaskListener接口 (实体域)
2. Task表示任务本身 (实体域)
3. TaskListener为事件通知,所有的UI操作均放在Listener里处理 (实体域)
4. TaskContext为互上下文,保存执行过程中的状态 (会话域)
5. TaskEvent为事件信息 (会话域)
6. TaskExecutor提供执行能力,当然TaskExecutor可以派生出TaskService做为SPI,这样就可以同时兼容Swing/AWT和JFace/SWT (服务域)

接口签名如下:
/**
 * 任务接口
 * 
 * @author 梁飞
 */
public interface Task {

	/**
	 * 执行任务.
	 * 非UI的工作,全部放在该方法内做,包括远程调用,数据转换,数据检验等,
	 * 其它,UI相关的工作,放在TaskListner中处理。
	 * 
	 * @param context 任务上下文状态
	 */
	void execute(TaskContext context);

}


/**
 * 任务标题
 * 
 * @author 梁飞
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TaskTitle {
	
	/**
	 * 标题
	 * 
	 * @return 标题
	 */
	String value();

}


/**
 * 任务上下文 (非线程安全,只在一次执行过程中使用)
 * 
 * @author 梁飞
 */
public interface TaskContext {
	
	/**
	 * 保存数据
	 * 
	 * @param key 键
	 * @param value 值
	 */
	void put(String key, Object value);
	
	/**
	 * 获取数据
	 * 
	 * @param <T> 值类型
	 * @param key 键
	 * @return 值
	 */
	<T> T get(String key);
	
	/**
	 * 设置当前运行信息
	 * 
	 * @param message 当前运行信息
	 */
	void setMessage(String message);
	
	/**
	 * 获取当前运行信息
	 * 
	 * @return 当前运行信息
	 */
	String getMessage();

	/**
	 * 设置进度
	 * 
	 * @param progress 进度
	 */
	void setProgress(int progress);
	
	/**
	 * 获取进度
	 * 
	 * @return 进度
	 */
	int getProgress();

	/**
	 * 取消执行
	 */
	void cancel();
	
	/**
	 * 是否已取消
	 * 
	 * @return 是否已取消
	 */
	boolean isCancelled();
	
	/**
	 * 是否失败
	 * 
	 * @return 是否失败
	 */
	boolean isFailed();

	/**
	 * 是否成功
	 * 
	 * @return 是否成功
	 */
	boolean isSucceeded();
	
	/**
	 * 是否结束
	 * 
	 * @return 是否结束
	 */
	boolean isExecuted();
	
}


/**
 * 任务执行服务 (该接口实现类必需保证线程安全)
 * 
 * @author 梁飞
 */
public interface TaskService {

	/**
	 * 执行任务
	 * 
	 * @param task 任务
	 * @return 任务上下文
	 */
	TaskContext execute(final Task task);

}


/**
 * 任务执行器 (线程安全)
 * 
 * @author 梁飞
 */
public class TaskExecutor {

	// 任务执行者服务
	private static TaskService TASK_SERVICE = new SwingTaskService();

	/**
	 * 设置任务执行者服务
	 * 
	 * @param taskService 任务执行者服务
	 */
	public static void setTaskService(TaskService taskService) {
		if (taskService == null) {
			throw new IllegalArgumentException("taskService == null");
		}
		TASK_SERVICE = taskService;
	}

	/**
	 * 执行任务
	 * 
	 * @param task 任务
	 * @return 任务上下文
	 */
	public static TaskContext execute(Task task) {
		return TASK_SERVICE.execute(task);
	}

}


/**
 * 任务事件监听器
 * 
 * @author 梁飞
 */
public interface TaskListener extends java.util.EventListener {

	/**
	 * 开始执行
	 * 
	 * @param event 事件信息
	 */
	void executing(TaskEvent event);

	/**
	 * 执行结束 (不管是成功,失败,或取消,此方法均被调用)
	 * 
	 * @param event 事件信息
	 */
	void executed(TaskEvent event);

	/**
	 * 执行成功
	 * 
	 * @param event 事件信息
	 */
	void succeeded(TaskEvent event);

	/**
	 * 执行失败
	 * 
	 * @param event 事件信息
	 */
	void failed(TaskEvent event);

	/**
	 * 取消执行
	 * 
	 * @param event 事件信息
	 */
	void cancelled(TaskEvent event);
	
	/**
	 * 后台执行
	 * 
	 * @param event 事件信息
	 */
	void backgrounded(TaskEvent event);

	/**
	 * 前台执行
	 * 
	 * @param event 事件信息
	 */
	void foregrounded(TaskEvent event);

	/**
	 * 值变化
	 * 
	 * @param event 事件信息
	 */
	void valueChanged(TaskEvent event);

	/**
	 * 信息变化
	 * 
	 * @param event 事件信息
	 */
	void messageChanged(TaskEvent event);

	/**
	 * 进度变化
	 * 
	 * @param event 事件信息
	 */
	void progressChanged(TaskEvent event);

}


/**
 * 任务事件信息
 * 
 * @author 梁飞
 */
public class TaskEvent extends java.util.EventObject {

	private static final long serialVersionUID = -7251403985319158057L;

	private final Task task;
    
    private final TaskContext taskContext;

    public TaskEvent(Object source, Task task, TaskContext taskContext) {
		super(source);
		this.task = task;
		this.taskContext = taskContext;
	}

    /**
     * 获取事件所属任务
     * 
     * @return 任务
     */
	public Task getTask() {
		return task;
	}

	/**
     * 获取事件所在任务上下文
     * 
     * @return 任务上下文
     */
	public TaskContext getTaskContext() {
		return taskContext;
	}

}


/**
 * 事件监听器适配
 * 
 * @author 梁飞
 */
public class TaskAdapter implements TaskListener {

	@Override
	public void executing(TaskEvent event) {
	}

	@Override
	public void executed(TaskEvent event) {
	}

	@Override
	public void succeeded(TaskEvent event) {
	}

	@Override
	public void failed(TaskEvent event) {
	}

	@Override
	public void cancelled(TaskEvent event) {
	}

	@Override
	public void backgrounded(TaskEvent event) {
	}

	@Override
	public void foregrounded(TaskEvent event) {
	}

	@Override
	public void valueChanged(TaskEvent event) {
	}

	@Override
	public void messageChanged(TaskEvent event) {
	}

	@Override
	public void progressChanged(TaskEvent event) {
	}

}


/**
 * 任务基类
 * 
 * @author 梁飞
 */
public abstract class TaskSupport extends TaskAdapter implements Task {
}
分享到:
评论

相关推荐

    OPC客户端(C#完整源码)

    9. **UI更新**:在GUI应用程序中,OPC数据的变化需要实时反映在界面上。C#的`InvokeRequired`和`Invoke`方法可用于在UI线程上安全地更新控件。 10. **日志记录**:为了调试和故障排查,客户端可能包含日志记录功能...

    DotNetty系列六:将服务端和客户端改为Winform窗口,博文项目源码

    将服务端和客户端改为Winform窗口”指的是一个关于使用DotNetty框架的教程,该教程将展示如何将DotNetty应用的服务端和客户端集成到Windows Forms(Winform)应用程序中,以创建具有图形用户界面(GUI)的网络通信...

    C#在线聊天系统(客户端+服务器)

    5. **多线程编程**:为了同时处理多个客户端的连接和请求,服务器端通常会使用多线程或者异步编程模型,如C#的Task或者async/await关键字。 6. **用户界面设计**:客户端的UI设计可能使用Windows Forms或WPF,提供...

    c#网络游戏象棋客户端

    3. **图形用户界面(GUI)设计**:客户端需要一个直观且吸引人的界面,让玩家可以轻松操作。C#的Windows Forms或WPF(Windows Presentation Foundation)框架提供了创建GUI的工具。设计师会使用控件如Button、...

    C#ZIP压缩客户端程序

    C#的System.Threading命名空间提供了Thread和Task类,可以用来实现异步操作,提高用户体验。 10. **文件对话框**: 让用户选择要压缩的文件或目录时,可以使用FileDialog控件。C#的System.Windows.Forms命名空间...

    dstask-gui

    dstask-gui(占位符) 该项目的名称是一个占位符,非常欢迎提出建议。什么这是之上的HTTP API层和Web前端。 这是因为我在使用手机时需要管理任务。 它通过将dstask用作库来执行此操作,而不仅仅是exec dstask。 现在...

    嵌入式Linux系统中的GUI系统的研究与移植.doc

    总的来说,嵌入式Linux系统中的GUI系统研究与移植是一项复杂的工作,涉及到操作系统的选择、硬件资源的配置、软件设计以及GUI库的适配。通过精心的设计和实现,可以打造出功能丰富、用户体验良好的嵌入式产品,如短...

    基于C#的即时通讯客户端源码.zip

    3. GUI编程:C#中的Windows Forms和WPF(Windows Presentation Foundation)框架用于构建图形用户界面。本项目可能使用了其中的一种来实现即时通讯的界面设计。 【即时通讯原理】 1. TCP/IP协议:即时通讯通常基于...

    c#关于绘图示例,白板服务器,白板客户端,一个画图板

    在C#编程环境中,开发一个绘图示例、白板服务器和白板客户端涉及到多个关键知识点,这包括图形用户界面(GUI)设计、网络通信、多线程处理以及事件驱动编程。下面将详细阐述这些核心概念。 首先,C#中的绘图功能...

    OPC客户端(C#完整源码)_《0528》.rar

    8. 多线程编程:因为OPC通信通常涉及异步操作,所以理解C#中的多线程和异步编程模型,如Task和async/await关键字,对于实现高效客户端至关重要。 9. UI集成:如果客户端需要用户界面,那么还需要了解WPF或WinForms...

    Java-pic.zip_java界面设计

    8. **多线程**:在GUI编程中,为了保持界面的响应性,通常需要将耗时操作放在后台线程(如SwingWorker或JavaFX的Task)中执行,以免阻塞事件处理线程。 9. **数据绑定**:JavaFX提供了模型-视图-控制器(MVC)架构...

    聊天室程序Java课程设计.docx

    在本Java课程设计中,学生的目标是创建一个带界面的聊天室程序,旨在将传统的基于控制台的聊天程序转化为具有图形用户界面(GUI)的应用。这个设计不仅涉及到Java编程语言,还涵盖了网络通信、数据库技术和组件与...

    Java chatroom.rar_Chatroom_java聊天室

    在聊天室的设计中,服务器端需要维持一个客户端连接列表,当新的客户端连接时,将其加入列表并广播消息到所有在线的客户端。这一过程可能涉及到多线程,因为每个客户端连接都需要独立的线程来处理其发送和接收的数据...

    一个用C#写的聊天室源码

    5. **UI设计**:聊天室的用户界面(UI)可能使用Windows Forms或WPF框架构建,这两个都是C#提供的GUI库。开发者可能会使用TextBox控件显示聊天历史,Button控件触发发送消息,ListView或ListBox控件显示在线用户列表...

    应用展示了创建Kiosk(桌面锁定)应用时Android锁屏任务模式的基本使用

    在Android系统中,Kiosk模式,也称为自助服务模式或专有模式,是一种特殊的应用程序部署方式,主要用于商业、教育和公共场所,使设备只能运行特定的应用程序,限制用户对系统的其他部分进行访问。...

    C#聊天程序

    【C#聊天程序】是一个基于C/S(客户端/服务器)架构设计的课程项目,旨在教授学生如何使用C#编程语言创建功能完善的聊天应用程序。在这个项目中,开发者将学习到C#的核心特性,如面向对象编程、网络通信以及图形用户...

    用VS2005中C#做的聊天室程序

    5. WinForms或WPF GUI设计,控件的使用和事件处理。 6. 网络通信协议,如TCP/IP的基本原理和应用。 7. 数据序列化和反序列化,可能是字符串或自定义对象的转换。 8. 异步编程,提高服务器性能和用户体验。 对于初学...

    C#网络编程高级篇之网页游戏辅助程序设计软件包

    C#提供了System.Windows.FormsAutomation命名空间,可以用来控制GUI元素,实现自动化操作。 4. **多线程编程**:为了保证辅助程序的响应速度和效率,多线程编程是必不可少的。C#提供了丰富的多线程支持,如Thread、...

    javafx多线程实现界面实时刷新

    JavaFX是一种用于构建桌面、移动和嵌入式设备上的富客户端应用程序的现代Java GUI框架。在JavaFX中,处理耗时任务时,如大量数据计算或网络通信,如果不使用多线程,可能会导致用户界面(UI)冻结,用户体验下降。...

Global site tag (gtag.js) - Google Analytics