您的位置:首页 > 编程语言 > Java开发

Java实现Qt的SIGNAL-SLOT机制

2012-06-04 22:41 549 查看
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是不是很方便。多开拓自己的视野,借鉴优秀的思想,我们才能做出更好的设计!
希望你喜欢本文。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: