• Java实现Qt的SIGNALSLOT机制


    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是不是很方便。多开拓自己的视野,借鉴优秀的思想,我们才能做出更好的设计!
    希望你喜欢本文。

  • 相关阅读:
    JAVA 多态
    win10 快捷键
    MSTAR SETBOX 常用API
    MSTAR GUI
    APACHE2 服务器配置 (一)
    MSTAR SERVICE结构
    各个国家 不同字符集的unicode 编码范围
    PhpStorm中如何配置SVN,详细操作方法
    PHP/Javascript 数组定义 及JSON中的使用 ---OK
    The "Run One Program Only" Phenomenon
  • 原文地址:https://www.cnblogs.com/xiaomaohai/p/6157834.html
Copyright © 2020-2023  润新知