У меня есть приложение Swing в Windows 10. Также я добавил хук низкоуровневой клавиатуры, который должен перехватывать события клавиатуры и переназначать, например, кнопку «z» на кнопку «s».
Это необходимо для перехвата событий клавиатуры вне моего Java-приложения. Я реализовал его, используя версию JNA 5.6.0 и версию jna-platform 5.6.0. Он отлично работает, но не внутри моего приложения Swing.
Моя проблема в том, что когда хук включен, приложение Swing заблокировано. Я не могу нажать на любую кнопку J и даже закрыть Jframe.
Я предполагаю, что это как-то связано с потоками, но я очень слаб в потоках и многопоточности.
Воспроизводимый пример.
Класс ТестФраме:
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class TestFrame extends JFrame {
public static void main(String [] args) {
TestFrame frame = new TestFrame();
JTextField textField=new JTextField();
textField.setBounds(50,50, 150,20);
JButton button=new JButton("Click Here");
button.setBounds(50,100,95,30);
button.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
ReMapper reMapper = new ReMapper();
reMapper.reMapOn();
textField.setText("Is my frame locked?");
}
});
frame.add(button);
frame.add(textField);
frame.setSize(400,400);
frame.setLayout(null);
frame.setVisible(true);
}
}
Класс переназначения:
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.*;
public class ReMapper {
private static WinUser.HHOOK hHook;
final User32 user32Library = User32.INSTANCE;
WinDef.HMODULE hMod = Kernel32.INSTANCE.GetModuleHandle(null);
static WinUser.INPUT input = new WinUser.INPUT();
public void reMapOn() {
WinUser.LowLevelKeyboardProc keyboardHook = new WinUser.LowLevelKeyboardProc() {
@Override
public WinDef.LRESULT callback(int nCode, WinDef.WPARAM wParam, WinUser.KBDLLHOOKSTRUCT kbDllHookStruct) {
if (nCode >= 0) {
if (wParam.intValue() == WinUser.WM_KEYDOWN) {
if (kbDllHookStruct.vkCode == 90) { // 90 is key code = z
sendKey(83); // 83 is key code = s
return new WinDef.LRESULT(1);
}
}
}
Pointer ptr = kbDllHookStruct.getPointer();
long peer = Pointer.nativeValue(ptr);
return user32Library.CallNextHookEx(hHook, nCode, wParam, new WinDef.LPARAM(peer));
}
};
hHook = user32Library.SetWindowsHookEx(WinUser.WH_KEYBOARD_LL, keyboardHook, hMod, 0);
int result;
WinUser.MSG msg = new WinUser.MSG();
while ((result = user32Library.GetMessage(msg, null, 0, 0)) != 0) {
if (result == -1) {
break;
} else {
user32Library.TranslateMessage(msg);
user32Library.DispatchMessage(msg);
}
}
}
static void sendKey(int keyCode) {
input.type = new WinDef.DWORD(WinUser.INPUT.INPUT_KEYBOARD);
input.input.setType("ki"); // Because setting INPUT_INPUT_KEYBOARD is not enough: https://groups.google.com/d/msg/jna-users/NDBGwC1VZbU/cjYCQ1CjBwAJ
input.input.ki.wScan = new WinDef.WORD(0);
input.input.ki.time = new WinDef.DWORD(0);
input.input.ki.dwExtraInfo = new BaseTSD.ULONG_PTR(0);
// Press
input.input.ki.wVk = new WinDef.WORD(keyCode); // 0x41
input.input.ki.dwFlags = new WinDef.DWORD(0); // keydown
User32.INSTANCE.SendInput(new WinDef.DWORD(1), (WinUser.INPUT[]) input.toArray(1), input.size());
// Release
input.input.ki.wVk = new WinDef.WORD(keyCode); // 0x41
input.input.ki.dwFlags = new WinDef.DWORD(2); // keyup
User32.INSTANCE.SendInput(new WinDef.DWORD(1), (WinUser.INPUT[]) input.toArray(1), input.size());
}
}
Вот экран с заблокированным Jframe после нажатия кнопки «Нажмите здесь»:
Класс ReMapper отлично работает отдельно от приложения Swing.
reMapOn()
позволяет переназначить «z» на «s». Но мне нужно, чтобы он работал внутри моего приложения Swing, а не блокировал его.
Может кто знает в чем может быть проблема и как ее исправить?
Глядя на ваш код reMapOn
, вы видите, что он имеет цикл while, и это указывает на то, что он может работать бесконечно и блокировать пользовательский интерфейс приложений.
Что вам нужно сделать, так это просто в вашем методе addActionListener
вызвать метод reMapOn
в отдельном потоке. Это можно сделать с помощью простого Thread или Swing Worker:
Пример Swing Worker (предпочтительное решение, поскольку вы можете переопределить done
и при необходимости манипулировать компонентами Swing в этом методе после завершения переназначения):
new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
ReMapper reMapper = new ReMapper();
reMapper.reMapOn();
return null;
}
}.execute();
textField.setText("Is my frame locked?");
Пример темы:
new Thread(() -> {
ReMapper reMapper = new ReMapper();
reMapper.reMapOn();
}).start();
textField.setText("Is my frame locked?");
Некоторые другие моменты:
null
/AbsoluteLayout
, а используйте соответствующий LayoutManagersetBounds()
или setSize()
для компонентов, если вы используете правильный менеджер компоновки, это будет обработано за вас.JFrame#pack()
, прежде чем сделать рамку видимой при использовании LayoutManager
JFrame
без необходимостиSwingUtilities.invokeLater
Спасибо за помощь. Теперь это работает как шарм :)