Java实现Qt的SIGNAL-SLOT机制

时间:2022-08-09 08:20:15

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中,从而将它们关联起来。
  1. public static void connect(QObject sender, String signal, Object receiver, String slot) {
  2. if (sender.signalSlotMap == null)
  3. sender.signalSlotMap = new HashMap<String, List<ReceiverSlot>>();
  4. List<ReceiverSlot> slotList = sender.signalSlotMap.get(signal);
  5. if (slotList == null) {
  6. slotList = new LinkedList<ReceiverSlot>();
  7. sender.signalSlotMap.put(signal, slotList);
  8. }
  9. slotList.add(createReceiverSlot(receiver, slot));
  10. }
  1. static class ReceiverSlot {
  2. Object receiver;
  3. Method slot;
  4. Object[] args;
  5. }
 
2.在创建ReceiverSlot时,我们解析SLOT方法名,如将slot(String,String)解析为方法slot,参数两个String。
如果解析失败我们就认为该SLOT仍是一个信号,也就是SIGNAL-SIGNAL的连接。这种情况下,我们需要
传递调用的不是receiver的SLOT方法,而是emit方法继续发射信号。
  1. private static ReceiverSlot createReceiverSlot(Object receiver, String slot) {
  2. ReceiverSlot receiverSlot = new ReceiverSlot();
  3. receiverSlot.receiver = receiver;
  4. Pattern pattern = Pattern.compile("(\\w+)\\(([\\w+,]*)\\)");
  5. Matcher matcher = pattern.matcher(slot);
  6. if (matcher.matches() && matcher.groupCount() == 2) {
  7. // 1.Connect SIGNAL to SLOT
  8. try {
  9. String methodName = matcher.group(1);
  10. String argStr = matcher.group(2);
  11. ArrayList<String> argList = new ArrayList<String>();
  12. pattern = Pattern.compile("\\w+");
  13. matcher = pattern.matcher(argStr);
  14. while (matcher.find())
  15. argList.add(matcher.group());
  16. String[] arguments = argList.toArray(new String[0]);
  17. receiverSlot.slot = findMethod(receiver, methodName, arguments);
  18. receiverSlot.args = new Object[0];
  19. }
  20. catch (Exception e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. else {
  25. // 2.Connect SIGNAL to SIGNAL
  26. if (receiver instanceof QObject) {
  27. receiverSlot.slot = emitMethod;
  28. receiverSlot.args = new Object[] { slot };
  29. }
  30. }
  31. return receiverSlot;
  32. }
  1. private static Method emitMethod;
  2. protected Map<String, List<ReceiverSlot>> signalSlotMap;
  3. static {
  4. try {
  5. emitMethod = QObject.class.getDeclaredMethod("emit", String.class, Object[].class);
  6. } catch (Exception e) {
  7. e.printStackTrace();
  8. }
  9. }
 
3.解析后,如果是SIGNAL-SLOT的连接,那我我们根据方法名和参数找到该方法,准备反射调用。
  1. private static Method findMethod(Object receiver, String methodName, String[] arguments)
  2. throws NoSuchMethodException {
  3. Method slotMethod = null;
  4. if (arguments.length == 0)
  5. slotMethod = receiver.getClass().getMethod(methodName, new Class[0]);
  6. else {
  7. for (Method method : receiver.getClass().getMethods()) {
  8. // 1.Check method name
  9. if (!method.getName().equals(methodName))
  10. continue;
  11. // 2.Check parameter number
  12. Class<?>[] paramTypes = method.getParameterTypes();
  13. if (paramTypes.length != arguments.length)
  14. continue;
  15. // 3.Check parameter type
  16. boolean isMatch = true;
  17. for (int i = 0; i < paramTypes.length; i++) {
  18. if (!paramTypes[i].getSimpleName().equals(arguments[i])) {
  19. isMatch = false;
  20. break;
  21. }
  22. }
  23. if (isMatch) {
  24. slotMethod = method;
  25. break;
  26. }
  27. }
  28. if (slotMethod == null)
  29. throw new NoSuchMethodException("Cannot find method[" + methodName +
  30. "] with parameters: " + Arrays.toString(arguments));
  31. }
  32. return slotMethod;
  33. }
 
4.发射信号时,我们取到所有与该SIGNAL关联的ReceiverSlot类,逐个发射信号。
  1. protected void emit(String signal, Object... args) {
  2. System.out.println(getClass().getSimpleName() + " emit signal " + signal);
  3. if (signalSlotMap == null)
  4. return;
  5. List<ReceiverSlot> slotList = signalSlotMap.get(signal);
  6. if (slotList == null || slotList.isEmpty())
  7. return;
  8. for (ReceiverSlot objSlot : slotList) {
  9. try {
  10. if (objSlot.slot == emitMethod)
  11. objSlot.slot.invoke(objSlot.receiver, objSlot.args[0], args);
  12. else
  13. objSlot.slot.invoke(objSlot.receiver, args);
  14. }
  15. catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }
 
之后,我们实现一个它的子类QWidget,将常用的Swing控件都封装在QWidget的子类中,为这些控件提供
常见的预定义的SIGNAL,像Qt中的clicked和returnPressed。
 
QWidget.java
  1. public class QWidget<T extends JComponent> extends QObject implements QSwing<T> {
  2. protected T widget;
  3. public QWidget(Class<T> clazz) {
  4. try {
  5. widget = clazz.newInstance();
  6. }
  7. catch (Exception e) {
  8. e.printStackTrace();
  9. }
  10. }
  11. @Override
  12. public T getSwingWidget() {
  13. return this.widget;
  14. }
  15. }
 
以下是封装了JButton和JTextField的QWidget子类。
 
QPushButton.java
  1. public class QPushButton extends QWidget<JButton> {
  2. public static final String CLICKED = "clicked";
  3. public QPushButton(String text) {
  4. super(JButton.class);
  5. widget.setText(text);
  6. widget.addActionListener(new ActionListener() {
  7. @Override
  8. public void actionPerformed(ActionEvent e) {
  9. emit(CLICKED);
  10. }
  11. });
  12. }
  13. }
 
QLineEdit.java
  1. public class QLineEdit extends QWidget<JTextField> {
  2. public static final String RETURN_PRESSED = "returnPressed";
  3. public QLineEdit() {
  4. super(JTextField.class);
  5. widget.addActionListener(new ActionListener() {
  6. @Override
  7. public void actionPerformed(ActionEvent e) {
  8. emit(RETURN_PRESSED);
  9. }
  10. });
  11. }
  12. }
 
下面我们来写个测试类实验下Java版的SIGNAL-SLOT机制,依旧是之前的浏览器的例子。
 
AddressBar.java
  1. public class AddressBar extends QWidget<JPanel> {
  2. /**
  3. * SIGNAL
  4. */
  5. public static final String NEW_BUTTON_CLICKED = "newButtonClicked";
  6. public static final String GO_TO_ADDRESS = "goToAddress(String,String)";
  7. /**
  8. * SLOT
  9. */
  10. public static final String HANDLE_GO_TO_ADDRESS = "handleGoToAddress()";
  11. private QPushButton newButton;
  12. private QLineEdit addressEdit;
  13. private QPushButton goButton;
  14. public AddressBar() {
  15. super(JPanel.class);
  16. // 1.Create widget
  17. newButton = new QPushButton("New");
  18. addressEdit = new QLineEdit();
  19. goButton = new QPushButton("Go");
  20. // 2.Set property
  21. addressEdit.getSwingWidget().setColumns(10);
  22. // 3.Connect signal-slot
  23. connect(newButton, QPushButton.CLICKED, this, NEW_BUTTON_CLICKED);
  24. connect(addressEdit, QLineEdit.RETURN_PRESSED, this, HANDLE_GO_TO_ADDRESS);
  25. connect(goButton, QPushButton.CLICKED, this, HANDLE_GO_TO_ADDRESS);
  26. // 4.Add to layout
  27. getSwingWidget().add(newButton.getSwingWidget());
  28. getSwingWidget().add(addressEdit.getSwingWidget());
  29. getSwingWidget().add(goButton.getSwingWidget());
  30. }
  31. public void handleGoToAddress() {
  32. emit(GO_TO_ADDRESS, addressEdit.getSwingWidget().getText(), "test string");
  33. }
  34. }
 
TabBar.java
  1. public class TabBar extends JTabbedPane {
  2. /**
  3. * SLOT
  4. */
  5. public static final String HANDLE_NEW_TAB = "handleNewTab()";
  6. public static final String HANDLE_GO_TO_SITE = "goToSite(String,String)";
  7. public TabBar() {
  8. handleNewTab();
  9. }
  10. public void handleNewTab() {
  11. WebView tab = new WebView();
  12. add("blank", tab);
  13. }
  14. public void goToSite(String url, String testStr) {
  15. System.out.println("Receive url: " + url + ", " + testStr);
  16. WebView tab = (WebView) getSelectedComponent();
  17. tab.load(url);
  18. }
  19. }
 
MainWindow.java
  1. public class MainWindow extends JFrame {
  2. public static void main(String[] args) {
  3. JFrame window = new MainWindow();
  4. window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  5. window.setSize(320, 340);
  6. window.setVisible(true);
  7. }
  8. public MainWindow() {
  9. // 1.Create widget
  10. AddressBar addressBar = new AddressBar();
  11. TabBar tabBar = new TabBar();
  12. // 2.Set property
  13. // 3.Connect signal-slot
  14. QObject.connect(addressBar, AddressBar.NEW_BUTTON_CLICKED, tabBar, TabBar.HANDLE_NEW_TAB);
  15. QObject.connect(addressBar, AddressBar.GO_TO_ADDRESS, tabBar, TabBar.HANDLE_GO_TO_SITE);
  16. // 4.Add to layout
  17. GridBagLayout layout = new GridBagLayout();
  18. setLayout(layout);
  19. GridBagConstraints grid = new GridBagConstraints();
  20. grid.fill = GridBagConstraints.BOTH;
  21. grid.gridx = grid.gridy = 0;
  22. grid.weightx = 1.0;
  23. grid.weighty = 0.1;
  24. add(addressBar.getSwingWidget(), grid);
  25. grid.fill = GridBagConstraints.BOTH;
  26. grid.gridx = 0;
  27. grid.gridy = 1;
  28. grid.weightx = 1.0;
  29. grid.weighty = 0.9;
  30. add(tabBar, grid);
  31. }
  32. }
  33. @SuppressWarnings("serial")
  34. class WebView extends JEditorPane {
  35. public WebView() {
  36. setEditable(false);
  37. }
  38. public void load(final String url) {
  39. SwingUtilities.invokeLater(new Runnable() {
  40. @Override
  41. public void run() {
  42. try {
  43. WebView.this.setPage(url);
  44. } catch (IOException e) {
  45. e.printStackTrace();
  46. }
  47. }
  48. });
  49. }
  50. }
 
测试一下吧,运行起来的效果就是这样。
Java实现Qt的SIGNAL-SLOT机制
 
新建Tab页和前往该地址事件都可以成功地从AddressBar传递到TabBar。怎么样,这种Java版的
SIGNAL-SLOT是不是很方便。多开拓自己的视野,借鉴优秀的思想,我们才能做出更好的设计!
希望你喜欢本文。
 
参考:http://blog.csdn.net/dc_726/article/details/7632430