关于Windows上的java:SWT:光标处的滚动控件(不聚焦)

SWT on Windows: scroll control at cursor (not focused one)

我们正在将Swing应用程序转换为SWT,并且已经可用。使我真正发疯的是,在Windows上使用SWT(与Swing相比),即使鼠标指针悬停在另一个控件上,也仅滚动了焦点所在的控件(例如表,列表,多行文本字段)。

是否有可能在我们的应用程序中更改此行为(不必安装第三方实用程序),例如通过为滚动事件安装一些与控件无关的钩子/过滤器,可以将事件重定向到当前光标位置的控件,也可以先自动移动焦点。预先感谢。


原始解决方案存在许多问题。

  • 它应该使用Reflection(它自己提出)。
  • 它应该沿小部件层次结构向上移动以找到一个父小部件,该父小部件应处理wheel事件,而不是鼠标下的实际小部件。这是必需的,因为如果鼠标下的窗口小部件未设置SWT.V_SCROLL或SWT.H_SCROLL样式位,并且不包括相应的本机ScrollBar小部件,则将不处理事件。
  • 此外,鼠标下的窗口小部件或其父窗口小部件之一可能已为SWT.MouseWheel附加了侦听器。可以省去假设是要在这些侦听器中处理SWT.MouseWheel事件,因此尽管平台没有在没有滚动条的情况下不会向这些小部件传递wheel事件的事实,但开发人员可能希望这些小部件能够接收事件。

下面是准备复制的代码片段,它基于原始答案但可以解决所有这些问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.widgets.Scrollable;

/**
 * The standard platform behavior on Windows is to scroll the widget with
 * keyboard focus when the user turns the mouse wheel, instead of the widget
 * currently under the mouse pointer. Many consider this annoying and Windows
 * itself, as well as many popular Windows software, breaks this rule and
 * implements the behavior seen on other platforms, which is to scroll the
 * widget under the mouse.
 *
 * Win32MouseWheelFilter is a Listener implementation which will filter for
 * SWT.MouseWheel events delivered to any Widget and try to redirect the event
 * to the widget under the mouse or one of it's parents. The widget, or one of
 * it's parents is considered a suitable target, if it either has Listeners for
 * SWT.MouseWheel attached (assuming that those listeners would do something
 * sensible with the event), or if its style bits contain SWT.H_SCROLL and/or
 * SWT.V_SCROLL. In the later case a low level system event is generated, which
 * is necessary to get the event handled by the native ScrollBar widgets. A
 * vertical ScrollBar is preferred as the target, unless it is for some reason
 * unsuitable for scrolling. In that case, horizontal scrolling would take
 * place, if there is a suitable horizontal ScrollBar.
 *
 * Simply creating a new Win32MouseWheelFilter instance will install it as an
 * event filter in the Display passed to the constructor. At an appropriate
 * time, you may call dispose() to remove the filter again. On SWT platforms
 * other than"win32", constructing an Win32MouseWheelFilter will have no effect.
 */

public class Win32MouseWheelFilter implements Listener {

    private final Display   fDisplay;

    private int             WM_VSCROLL;
    private int             WM_HSCROLL;
    private int             SB_LINEUP;
    private int             SB_LINEDOWN;

    private Method          fSendEventMethod32;
    private Method          fSendEventMethod64;

    /**
     * Creates a new Win32MouseWheelFilter instance and registers it as global
     * event filter in the provided Display. Nothing will happen if the SWT
     * platform is not"win32". If for some reason some SWT internals have
     * changed since the writing of this class, and the Reflection-based
     * extraction of some win32 specific fields of the SWT OS class fails,
     * no filtering of wheel events will take place either.
     *
     * @param display
     *      The Display instance that the Win32MouseWheelFilter should install
     *      itself into as global event filter.
     */

    public Win32MouseWheelFilter(Display display) {
        fDisplay = display;

        if (!SWT.getPlatform().equals("win32"))
            return;

        try {
            Class< ? > os = Class.forName("org.eclipse.swt.internal.win32.OS");
            WM_VSCROLL = os.getDeclaredField("WM_VSCROLL").getInt(null);
            WM_HSCROLL = os.getDeclaredField("WM_HSCROLL").getInt(null);
            SB_LINEUP = os.getDeclaredField("SB_LINEUP").getInt(null);
            SB_LINEDOWN = os.getDeclaredField("SB_LINEDOWN").getInt(null);

            try {
                // Try the 32-bit version first
                fSendEventMethod32 = os.getDeclaredMethod("SendMessage",
                    int.class, int.class, int.class, int.class);
            } catch (NoSuchMethodException e) {
                // Fall back to the 64-bit version
                fSendEventMethod64 = os.getDeclaredMethod("SendMessage",
                    long.class, int.class, long.class, long.class);
            }

            display.addFilter(SWT.MouseWheel, this);
            return;

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }

        System.out.println("Warning: Running on win32 SWT platform,"
            +"but unable to install Win32MouseWheelFilter filter.");
    }

    /**
     * If the receiver had previously installed itself as global event filter,
     * this method will remove it again from the display's filters.
     */

    public final void dispose() {
        fDisplay.removeFilter(SWT.MouseWheel, this);
    }

    public final void handleEvent(Event event) {
        Control cursorControl = event.display.getCursorControl();
        if (event.widget == cursorControl || cursorControl == null)
            return;

        if (event.widget instanceof Control) {
            // If the original target control's bounds contain the mouse
            // location, do not re-target the event, since it may indeed be the
            // Control that needs to handle scrolling for an embedded Control
            // that has focus.
            Control control = (Control) event.widget;
            Rectangle bounds = control.getBounds();
            bounds.x = 0;
            bounds.y = 0;
            Point cursorPos = control.toControl(display.getCursorLocation());
            if (bounds.contains(cursorPos))
                return;
        }

        // Try to find the best target widget for the event, based on the
        // cursorControl. A suitable target control is either one that has
        // a listener for SWT.MouseWheel attached, or one that has either
        // SWT.H_SCROLL or SWT.V_SCROLL in its style bits.
        Control wheelControl = cursorControl;
        int scrollStyle = SWT.H_SCROLL | SWT.V_SCROLL;
        while (wheelControl != null
            && (wheelControl.getStyle() & scrollStyle) == 0
            && wheelControl.getListeners(SWT.MouseWheel).length == 0) {
            wheelControl = wheelControl.getParent();
        }
        if (wheelControl == null) {
            // The event would not be handled by anyone, bail out.
            return;
        }

        int style = wheelControl.getStyle();

        if ((style & scrollStyle) != 0 && wheelControl instanceof Scrollable) {
            // Construct the data for the low level event based on which
            // direction the target can scroll in. We need to use a low-level
            // event since otherwise it won't be handled by the native
            // ScrollBar widgets.
            int msg;

            // Prefer vertical scrolling. However, if the
            // there is no vertical ScrollBar, or if it's somehow disabled,
            // then switch to horizontal scrolling instead.
            if ((style & SWT.V_SCROLL) != 0 ) {
                ScrollBar vBar = ((Scrollable) wheelControl).getVerticalBar();
                if (vBar == null
                    || ((vBar.getMinimum() == 0
                        && vBar.getMaximum() == 0
                        && vBar.getSelection() == 0)
                            || !vBar.isEnabled()
                            || !vBar.isVisible())) {
                    // There is no vertical ScrollBar, or it can't be used.
                    msg = WM_HSCROLL;
                } else
                    msg = WM_VSCROLL;
            } else {
                msg = WM_HSCROLL;
            }

            int count = event.count;
            int wParam = SB_LINEUP;
            if (event.count < 0) {
                count = -count;
                wParam = SB_LINEDOWN;
            }

            try {
                // Obtain the control's handle via Reflection and
                // deliver the event using the low level platform method.
                // (64 and 32 bit versions)
                if (fSendEventMethod32 != null) {
                    int handle = org.eclipse.swt.widgets.Control.class
                        .getDeclaredField("handle").getInt(wheelControl);
                    for (int i = 0; i < count; i++)
                        fSendEventMethod32.invoke(null, handle, msg, wParam, 0);
                } else {
                    long handle = org.eclipse.swt.widgets.Control.class
                        .getDeclaredField("handle").getLong(wheelControl);
                    for (int i = 0; i < count; i++)
                        fSendEventMethod64.invoke(null, handle, msg, wParam, 0);
                }

            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (SecurityException e) {
                e.printStackTrace();
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            }
        } else {
            // It makes no sense using the low-level OS event delivery, since
            // Widgets without the scrolling style bits won't receive this
            // event. Since we selected this widget based on the fact that it
            // has SWT.MouseWheel listeners attached, use the regular SWT event
            // notification system.

            // Convert mouse location, since the event contains it in the wrong
            // coordinate space (the one of the original event target).
            Point cursorPos = wheelControl.toControl(
                event.display.getCursorLocation());
            event.x = cursorPos.x;
            event.y = cursorPos.y;

            event.widget = wheelControl;
            wheelControl.notifyListeners(event.type, event);
        }

        // We re-targeted the event, or re-posted a new event to another widget,
        // so prevent this event from being processed any further.
        event.type = SWT.None;
        event.doit = false;
    }
}


在Windows上,我使用以下类来解决此问题。我几年前在某个地方找到它了,不再记得了(我可能已经反编译了ru.nlmk.utilplugins):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class AutoMouseWheelAdapter implements Listener {

   int WM_VSCROLL  = OS.WM_VSCROLL;
   int WM_HSCROLL  = OS.WM_HSCROLL;
   int SB_LINEUP   = OS.SB_LINEUP;
   int SB_LINEDOWN = OS.SB_LINEDOWN;

   public AutoMouseWheelAdapter() {
      if ( SWT.getPlatform().equals("win32") ) {
         Display.getCurrent().addFilter(SWT.MouseWheel, this);
      }
   }

   public void handleEvent( Event event ) {
      Control cursorControl = Display.getCurrent().getCursorControl();

      if ( event.widget == cursorControl || cursorControl == null ) {
         return;
      }

      event.doit = false;
      int msg = WM_VSCROLL;
      int style = cursorControl.getStyle();

      if ( (style & SWT.V_SCROLL) != 0 && cursorControl instanceof Scrollable ) {
         ScrollBar verticalBar = ((Scrollable)cursorControl).getVerticalBar();

         if ( verticalBar != null
            && ((verticalBar.getMinimum() == 0 && verticalBar.getMaximum() == 0 && verticalBar.getSelection() == 0) || !verticalBar.isEnabled() || !verticalBar
                  .isVisible()) ) {
            msg = WM_HSCROLL;
         }
      }
      else if ( (style & SWT.H_SCROLL) == 0 ) {
         return;
      }
      else {
         msg = WM_HSCROLL;
      }

      int count = event.count;
      int wParam = SB_LINEUP;

      if ( event.count < 0 ) {
         count = -count;
         wParam = SB_LINEDOWN;
      }

      for ( int i = 0; i < count; i++ ) {
         OS.SendMessage(cursorControl.handle, msg, wParam, 0);
      }
   }
}

创建显示线程后,只需在代码中添加new AutoMouseWheelAdapter(),或者删除构造函数并将其注册为自己的过滤器。

显然,此实现取决于Win32 SWT。如果您不希望编译时依赖,则将方法调用和字段package在反射调用中。


我们在上面使用stippi的解决方案(谢谢!)。除了一个例外-我们发现,如果光标完全位于应用程序窗口之外(至少在Windows 7上是这样),鼠标滚轮事件仍会传递给控件。 (显然)这是不希望的,因此我们对上述解决方案进行了较小的改进。

替换此代码:

1
2
3
    if ( event.widget == cursorControl || cursorControl == null ) {
         return;
    }

与此:

1
2
3
4
5
6
7
8
    if (cursorControl == null) {
        // The cursor is not in our display window, so prevent this event from being processed any further.
        event.type = SWT.None;
        event.doit = false;
        return;
    }
    if (event.widget == cursorControl)
        return;