Logitech SetPoint WM_MOUSEHWHEEL bugs

Logitech's SetPoint mouse drivers have serious mistakes in their handling of horizontal scroll-wheel messages (WM_MOUSEHWHEEL).

The mistakes cause problems in many applications and I have personally wasted hours, if not days, battling them in my own code. I share my findings here to help other developers.

Note 1 -- WM_MOUSEWHEEL vs WM_MOUSEHWHEEL:

  • Unfortunately, WM_MOUSEWHEEL and WM_MOUSEHWHEEL look very similar. Logitech's mistakes only apply to the latter, horizontal case (WM_MOUSE**H**WHEEL) and their handling of the more common vertical case seems completely fine.

Note 2 -- Links to MSDN API documents:

  • I apologise in advance for when the various links to MSDN API documentation become broken. Despite their use of numeric components which render them arbitrary and meaningless to humans in the first place, links to Microsoft-run web servers are inevitably prone to breakage in the wake of the company's insatiable, inexplicable and inexcusable lust for moving content around without any kind of forwarding. Clearly, Microsoft are still stuggling to grasp the concept of these new-fangled hyperlinks and this whole 'world wide web' thing, but that is outside of my control... Anyway, today I'm here to rant about Logitech instead.

Back on topic...

I first observed the Logitech WM_MOUSEHWHEEL problems some years ago (2009, SetPoint 4.80, Windows Vista 32-bit) and I believe they remain the same today (2012, SetPoint 6.32, Windows 7 64-bit). It is possible that some of the finer details have changed, but the main observations and workarounds should still be good and remain in use by code that I maintain and use every day with my Logitech mouse.

The WM_MOUSEHWHEEL problems are as follows:

  1. Logitech send WM_MOUSEHWHEEL to the wrong window:
    • Scroll-wheel messages are supposed to go to the control with the input focus. (Applications that wish to behave differently can then re-route the message themselves. It is not the job of the mouse driver to do that re-routing.)
    • While Logitech do the right thing with vertical wheel messages, they send horizontal wheel messages to whichever control is under the mouse. They do this even if the control does not have the focus. They do this even if the application does not have the focus.
    • This can be particularly troublesome when the message is sent to controls which can never gain the input focus and thus do not anticipate receiving scroll-wheel messages. (e.g. The ListView header control.)
  2. Logitech wrongly interpret the WM_MOUSEHWHEEL return value:
    • MSDN clearly states: If an application processes this message, it should return zero.
    • While Logitech get it right with vertical messages, they do the opposite with horitontal messages. Applications that handle WM_MOUSEHWHEEL need to return non-zero to tell Logitech's drivers that they have handled the message.
  3. Logitech wrongly convert WM_MOUSEHWHEEL into WM_HSCROLL if you return zero:
    • MSDN clearly states: The DefWindowProc function propagates the [WM_MOUSEHWHEEL] message to the window's parent. There should be no internal forwarding of the message, since DefWindowProc propagates it up the parent chain until it finds a window that processes it.
    • Let me describe the standard Win32 behaviour in more detail:
      • A window which does not handle WM_MOUSEHWHEEL should (as with any unhandled message) pass it to DefWindowProc. DefWindowProc will, in turn, send the message to the parent window, where the process repeats.
      • A window which handles WM_MOUSEHWHEEL should do whatever it needs to do (e.g. scroll something) and then return without calling DefWindowProc. By not calling DefWindowProc, the window "swallows" the message; that is, it ensures it is not propagated to or processed by any further windows.
      • (At least in this respect, it does not actually matter what the WM_MOUSEHWHEEL return value is. It only matters whether or not DefWindowProc is called. I believe that DefWindowProc will always return zero for the message, but I have not checked and it should not matter.)
      • If no window swallows the message, DefWindowProc will run out of parent windows and might then decide to turn the message into WM_HSCROLL or whatever, but that is up to DefWindowProc and the Win32 API itself, not something the mouse driver should be involved with.
      • (Note: WM_HSCROLL messages are not propagated the way mouse-wheeel messages are. If WM_HSCROLL is sent to a window then it is explicitly for that window and no other.)
    • Instead of -- or rather, in addition to -- those rules, Logitech do the following:
      • If the initial WM_MOUSEHWHEEL returns TRUE (non-zero) then nothing extra is done; just the standard Win32 behaviour.
      • If the initial WM_MOUSEHWHEEL returns FALSE (zero) then you get the standard Win32 behaviour, followed by Logitech's drivers generating an extra WM_HSCROLL message which is sent to an arbitrary window. (It may be the the window under the mouse but I am not certain.)
    • So, say your application does things properly, as per the API docs. You usually ignore WM_MOUSEHWHEEL and let DefWindowProc handle it. In a few special places, you handle it yourself and return FALSE (zero). But Logitech are also sending extra WM_HSCROLL messages to an arbitrary window, and that window may or may not be the same one that handled the original WM_MOUSEHWHEEL message. Your control may end up scrolling twice as far as it should or responding twice to each wheel movement. Or you may find your control scrolls in addition to some other control/element. Or your app may do something weird -- or even crash -- because WM_HSCROLL is being sent to something that never expected it.
    • That WM_HSCROLL should not be sent by the mouse drivers at all, let alone to the window Logitech are choosing, let alone when WM_MOUSEHWHEEL returns FALSE (zero).
  4. If you keep scrolling, Logitech may skip WM_MOUSEHWHEEL entirely and send WM_HSCROLL directly to one of your windows:
    • I have lost my notes on exactly what happens but, when testing your code, make sure you try holding down the horizontal-scroll button/wheel to generate repeated scroll messages. You may find that Logitech mess things up even more.
    • From memory, and possibly depending on the value you return, Logitech only send WM_MOUSEHWHEEL for the initial scroll and then cache the window that they choose to send WM_HSCROLL messages to. Repeated WM_HSCROLL messages are then delivered to the cached window directly until the tilt-wheel is released. Or something like that.
    • The Spy++ tool which comes with Visual Studio will help you work out WTF Logitech are sending to where.

How to work around this tragic mess:

  • Ignore what MSDN says and return TRUE for WM_MOUSEHWHEEL. By doing that you prevent Logitech's WM_HSCROLL messages, but only in cases where your code gets to WM_MOUSEHWHEEL before anything else passes it to DefWindowProc.
  • Subclass all controls, and all child-windows of those controls, to override their WM_MOUSEHWHEEL handling. Because Logitech send WM_MOUSEHWHEEL to whichever child window is under the mouse, and because Logitech start screwing the pooch the moment that window calls DefWindowProc or returns FALSE for the message, you have to ensure that your own code overrides and handles WM_MOUSEHWHEEL everywhere that it counts. That means you have to subclass every control, and every child window of every control, which can be scrolled horizontally and/or which will behave incorrectly if it starts receiving WM_HSCROLL messages. Where a window itself does not scroll but its parent needs to, you have to manually forward the message from parent to child, instead of relying on DefWindowProc*. Needless to say, this is a massive pain in the arse.

    (* I guess you could still call DefWindowProc, but always return TRUE afterwards. Same result, and not really any clearer or easier.)
  • Handle WM_MOUSEWHEEL and WM_MOUSEHWHEEL separately. A lot of the time, it makes sense to pass vertical and horizontal scroll messages to a common piece of code. Since you don't need (and probably don't want) to change the way vertical scrolling is performed and propagated, you will have to split your vertical and horizontal scroll-wheel handlers into separate cases. (They may still call a common function to do the main work, of course.)

Another Logitech mouse-driver issue, while I'm here:

  • Logitech do not check their wheel-delta values for overflows.
    • Wheel deltas are signed, 16-bit values so it does not take much for them to overflow. When they overflow, their signs are reversed. (Up becomes down and vice versa.)
    • With a free-spinning wheel (e.g. Performance MX) and/or high scroll-wheel acceleration, it is fairly easy to scroll fast enough that you cause an overflow. You start scrolling in one direction and then see the direction change by itself, (sometimes more than once).
    • Some applications may have this problem with any mouse driver (due to accumulatating wheel-deltas without checking for overflow) but Logitech's drivers have the bug built-in and cause the problem even in correct software.

A possible Logitech keyboard-driver issue, while I'm here:

  • Logitech's handling of AppCommand and Media Keys was atrocious the last time I checked (although that was several years ago and may no longer be true).
  • When you push a media key, like Play/Pause or Next Track, it is supposed to generate a WM_APPCOMMAND message (with APPCOMMAND_MEDIA_PLAY_PAUSE or APPCOMMAND_MEDIA_PREVIOUSTRACK or similar) which is sent to the application with focus. If the app with focus doesn't handle the message it is then meant to be propagated to other apps, and if none of them handle it then the OS (or keyboard driver) does its default handling.
  • Instead, at least last time I tried, Logitech's keyboard drivers had a configuration file (plus some default fallbacks) which mapped the media keys to different keypresses and hotkeys for different named applications. Rather than the proper WM_APPCOMMAND message and propagation, Logitech would decide which app/window should get the message and then directly send it WM_CHAR or WM_KEYDOWN messages (or similar; I forget the exact details). This would seriously confuse applications that didn't expect keyboard input when they didn't have the focus. It also meant the keys did not work in apps which handled WM_APPCOMMAND but were not in the config file.
  • Maybe this behaviour made sense before Windows itself had support for the media keys, but Logitech were still doing it a long time after that.
  • It is entirely possible that the media key behaviour is fixed now. While I still buy and use Logitech mice, the media-key handling is one of the reasons I have avoided their keyboards for the last few years. If it's all better now, please let me know, but only if you have a deep understanding of this stuff. "It seems to work fine when I push the keys" is not enough. I tried reporting this to Logitech's support at the time but they didn't even begin to understand what I was talking about. :(