Java实现Qt的SIGNAL-SLOT机制

SIGNAL-SLOT是Qt的一大特色,使用起来十分方便。在传统的AWT和Swing编程中,我们都是为要在

监听的对象上添加Listener监听器。被监听对象中保存有Listener的列表,当相关事件发生时,被监听
对象会通知所有Listener。而在Qt中,我们只需通过connect方法连接两个对象上的方法就可以了,非常
方便、优雅地实现了传统的观察者Observer模式。

Qt是如何办到的呢?对于发出SIGNAL的对象,我们需要在其头文件定义中声明Q_Object宏,之后Qt的
预处理器MOC会为我们自动添加上相应的代码来实现SIGNAL-SLOT机制。这与AspectJ自定义了Javac
编译器很类似,都是通过增强编译器来自动添加相应的代码。

增强编译或增加预处理太复杂,怎样能够简单的实现这种机制呢?首先我们实现一个类似的QObject类,
需要发射SIGNAL的类都要继承它。在QObject类中,我们自动为其子类提供监听器列表,查找SLOT方法,
信号发射等功能。

QObject.java

1.在连接方法中,我们将信号和新建的ReceiverSlot类保存到Map中,从而将它们关联起来。
	public static void connect(QObject sender, String signal, Object receiver, String slot) {
		if (sender.signalSlotMap == null)
			sender.signalSlotMap = new HashMap<String, List<ReceiverSlot>>();
		
		List<ReceiverSlot> slotList = sender.signalSlotMap.get(signal);
		if (slotList == null) {
			slotList = new LinkedList<ReceiverSlot>();
			sender.signalSlotMap.put(signal, slotList);
		}
		slotList.add(createReceiverSlot(receiver, slot));
	}
	static class ReceiverSlot {
		Object receiver;
		Method slot;
		Object[] args;
	}

2.在创建ReceiverSlot时,我们解析SLOT方法名,如将slot(String,String)解析为方法slot,参数两个String。
如果解析失败我们就认为该SLOT仍是一个信号,也就是SIGNAL-SIGNAL的连接。这种情况下,我们需要
传递调用的不是receiver的SLOT方法,而是emit方法继续发射信号。
	private static ReceiverSlot createReceiverSlot(Object receiver, String slot) {
		ReceiverSlot receiverSlot = new ReceiverSlot();
		receiverSlot.receiver = receiver;
		
		Pattern pattern = Pattern.compile("(\\w+)\\(([\\w+,]*)\\)");
		Matcher matcher = pattern.matcher(slot);
		if (matcher.matches() && matcher.groupCount() == 2) {
			// 1.Connect SIGNAL to SLOT
			try {
				String methodName = matcher.group(1);
				String argStr = matcher.group(2);
				ArrayList<String> argList = new ArrayList<String>();
				
				pattern = Pattern.compile("\\w+");
				matcher = pattern.matcher(argStr);
				while (matcher.find())
					argList.add(matcher.group());
				String[] arguments = argList.toArray(new String[0]);
				
				receiverSlot.slot = findMethod(receiver, methodName, arguments);
				receiverSlot.args = new Object[0];
			}
			catch (Exception e) {
				e.printStackTrace();
			}
		} 
		else {
			// 2.Connect SIGNAL to SIGNAL
			if (receiver instanceof QObject) {
				receiverSlot.slot = emitMethod;
				receiverSlot.args = new Object[] { slot };
			}
		}
		
		return receiverSlot;
	}
	private static Method emitMethod;
	
	protected Map<String, List<ReceiverSlot>> signalSlotMap;

	static {
		try {
			emitMethod = QObject.class.getDeclaredMethod("emit", String.class, Object[].class);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

3.解析后,如果是SIGNAL-SLOT的连接,那我我们根据方法名和参数找到该方法,准备反射调用。
	private static Method findMethod(Object receiver, String methodName, String[] arguments)
			throws NoSuchMethodException {
		
		Method slotMethod = null;
		
		if (arguments.length == 0) 
			slotMethod = receiver.getClass().getMethod(methodName, new Class[0]);
		else {
			for (Method method : receiver.getClass().getMethods()) {
				
				// 1.Check method name
				if (!method.getName().equals(methodName))
					continue;
				
				// 2.Check parameter number
				Class<?>[] paramTypes = method.getParameterTypes();
				if (paramTypes.length != arguments.length)
					continue;
				
				// 3.Check parameter type
				boolean isMatch = true;
				for (int i = 0; i < paramTypes.length; i++) {
					if (!paramTypes[i].getSimpleName().equals(arguments[i])) {
						isMatch = false;
						break;
					}
				}
				if (isMatch) {
					slotMethod = method;
					break;
				}
			}
			
			if (slotMethod == null)
				throw new NoSuchMethodException("Cannot find method[" + methodName + 
						"] with parameters: " + Arrays.toString(arguments));
		}
		
		return slotMethod;
	}

4.发射信号时,我们取到所有与该SIGNAL关联的ReceiverSlot类,逐个发射信号。
	protected void emit(String signal, Object... args) {
		System.out.println(getClass().getSimpleName() + " emit signal " + signal);
		
		if (signalSlotMap == null)
			return;
		
		List<ReceiverSlot> slotList = signalSlotMap.get(signal);		
		if (slotList == null || slotList.isEmpty())
			return;
		
		for (ReceiverSlot objSlot : slotList) {
			try {
				if (objSlot.slot == emitMethod)
					objSlot.slot.invoke(objSlot.receiver, objSlot.args[0], args);
				else
					objSlot.slot.invoke(objSlot.receiver, args);
			}
			catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

之后,我们实现一个它的子类QWidget,将常用的Swing控件都封装在QWidget的子类中,为这些控件提供
常见的预定义的SIGNAL,像Qt中的clicked和returnPressed。

QWidget.java
public class QWidget<T extends JComponent> extends QObject implements QSwing<T> {

	protected T widget;
	
	public QWidget(Class<T> clazz) {
		try {
			widget = clazz.newInstance();
		} 
		catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	@Override
	public T getSwingWidget() {
		return this.widget;
	}

}

以下是封装了JButton和JTextField的QWidget子类。

QPushButton.java
public class QPushButton extends QWidget<JButton> {

	public static final String CLICKED = "clicked";
	
	public QPushButton(String text) {
		super(JButton.class);
		
		widget.setText(text);
		widget.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				emit(CLICKED);
			}
		});
	}

}

QLineEdit.java
public class QLineEdit extends QWidget<JTextField> {

	public static final String RETURN_PRESSED = "returnPressed";
	
	public QLineEdit() {
		super(JTextField.class);
		
		widget.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				emit(RETURN_PRESSED);
			}
		});
	}

}

下面我们来写个测试类实验下Java版的SIGNAL-SLOT机制,依旧是之前的浏览器的例子。

AddressBar.java
public class AddressBar extends QWidget<JPanel> {
	
	/**
	 * SIGNAL
	 */
	public static final String NEW_BUTTON_CLICKED = "newButtonClicked";
	public static final String GO_TO_ADDRESS = "goToAddress(String,String)";
	
	/**
	 * SLOT
	 */
	public static final String HANDLE_GO_TO_ADDRESS = "handleGoToAddress()";
	
	
	private QPushButton newButton;
	private QLineEdit addressEdit;
	private QPushButton goButton;
	
	
	public AddressBar() {
		super(JPanel.class);
		
		// 1.Create widget
		newButton = new QPushButton("New");
		addressEdit = new QLineEdit();
		goButton = new QPushButton("Go");
		
		// 2.Set property
		addressEdit.getSwingWidget().setColumns(10);
		
		// 3.Connect signal-slot
		connect(newButton, QPushButton.CLICKED, this, NEW_BUTTON_CLICKED);
		connect(addressEdit, QLineEdit.RETURN_PRESSED, this, HANDLE_GO_TO_ADDRESS);
		connect(goButton, QPushButton.CLICKED, this, HANDLE_GO_TO_ADDRESS);
		
		// 4.Add to layout
		getSwingWidget().add(newButton.getSwingWidget());
		getSwingWidget().add(addressEdit.getSwingWidget());
		getSwingWidget().add(goButton.getSwingWidget());
	}
	
	public void handleGoToAddress() {
		emit(GO_TO_ADDRESS, addressEdit.getSwingWidget().getText(), "test string");
	}
	
}

TabBar.java
public class TabBar extends JTabbedPane {
	
	/**
	 * SLOT
	 */
	public static final String HANDLE_NEW_TAB = "handleNewTab()";
	public static final String HANDLE_GO_TO_SITE = "goToSite(String,String)";
	
	
	public TabBar() {
		handleNewTab();
	}
	
	public void handleNewTab() {
		WebView tab = new WebView();
		add("blank", tab);
	}
	
	public void goToSite(String url, String testStr) {
		System.out.println("Receive url: " + url + ", " + testStr);
		
		WebView tab = (WebView) getSelectedComponent();
		tab.load(url);
	}
	
}

MainWindow.java
public class MainWindow extends JFrame {

	public static void main(String[] args) {
		JFrame window = new MainWindow();
		window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		window.setSize(320, 340);
		window.setVisible(true);
	}
	
	public MainWindow() {
		// 1.Create widget
		AddressBar addressBar = new AddressBar();
		TabBar tabBar = new TabBar();
		
		// 2.Set property
		
		// 3.Connect signal-slot
		QObject.connect(addressBar, AddressBar.NEW_BUTTON_CLICKED, tabBar, TabBar.HANDLE_NEW_TAB);
		QObject.connect(addressBar, AddressBar.GO_TO_ADDRESS, tabBar, TabBar.HANDLE_GO_TO_SITE);
		
		// 4.Add to layout
		GridBagLayout layout = new GridBagLayout();
		setLayout(layout);
		GridBagConstraints grid = new GridBagConstraints();
		grid.fill = GridBagConstraints.BOTH;
		grid.gridx = grid.gridy = 0;
		grid.weightx = 1.0;
		grid.weighty = 0.1;
		add(addressBar.getSwingWidget(), grid);
		grid.fill = GridBagConstraints.BOTH;
		grid.gridx = 0;
		grid.gridy = 1;
		grid.weightx = 1.0;
		grid.weighty = 0.9;
		add(tabBar, grid);
	}
	
}


@SuppressWarnings("serial")
class WebView extends JEditorPane {
	
	public WebView() {
		setEditable(false);
	}
	
	public void load(final String url) {
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				try {
					WebView.this.setPage(url);
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		});
	}
	
}

测试一下吧,运行起来的效果就是这样。


新建Tab页和前往该地址事件都可以成功地从AddressBar传递到TabBar。怎么样,这种Java版的
SIGNAL-SLOT是不是很方便。多开拓自己的视野,借鉴优秀的思想,我们才能做出更好的设计!
希望你喜欢本文。

posted on 2012-06-04 22:41  毛小娃  阅读(195)  评论(0编辑  收藏  举报

导航