1

I have this code:

private void scrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
        {
            if (Keyboard.IsKeyDown(Key.LeftAlt))
            {
                e.Handled = true;

                //initiate zoooom!
            }                    
        }  

I wanted to implement alt+mousewheel for zooming reasons but I don't want to lose the regular mousewheel up/down on my scrollviewer. I just wanted in to disable it temporarily when zooming. I've tried unhandling at keyup and at the end of the of the method but still no luck.

The code above works like a toggle thing something. Alt+mousewheeling stops the scrollviewer from scrolling up/down but upon the release of alt the scrollviewer is still locked. It requires a second press of the alt to release the thing.

Sectoid
  • 13
  • 5
  • Have you tried move mousescrollevent to viewmodel command? Than take LeftAlt KeyBinding command, set flag that leftalt was pressed and see flag value in mousescroll command? – ElConrado Mar 29 '18 at 07:49
  • I wrote a WPF Behavior to do this in [this answer](https://stackoverflow.com/a/46427503/526724), it sounds like it would be useful to you. It even supports specifying a modifier key (like `LeftAlt`). – Bradley Uffner Mar 29 '18 at 11:00
  • @BradleyUffner Hi, I'm using Caliburn for this project. Actually I've already stumbled in that post before but I've doubts in using Interactivity. Is it safe to use alongside with Caliburn? – Sectoid Mar 29 '18 at 11:39
  • I've never used Caliburn before, but from the quick look I just took at it, I don't *think* it will break anything. If Caliburn provides its own `Behavior` pattern, I would try to adapt my code to what it uses though. – Bradley Uffner Mar 29 '18 at 12:26
  • @BradleyUffner Sadly your answer doesn't work in my case. Adding Interactivity in the project causes it not to compile correctly, it says: `Found conflicts between different versions of "System.Windows.Interactivity" that could not be resolved.` and running the program causes an parse exception thrown by Caliburn in PresentationFramework.dll. – Sectoid Mar 30 '18 at 12:49
  • It sounds like it might already be bringing in that assembly on its own. Try the behavior without bringing in the reference. – Bradley Uffner Mar 30 '18 at 12:50
  • @BradleyUffner Hmm... Intellisense seems don't detect anything related to `Behavior`. Creepy. – Sectoid Mar 30 '18 at 14:44

1 Answers1

2

The issue

The issue you are having is pressing Alt in a WPF application causes an in-built behaviour to move focus from the current item and into the Access Key Manager. To see this behaviour press Alt then Space and you will see your system menu pop up.

To see it more clearly add a button to your window with the content having an underscore before the character you would like to be your "access key"

<Button Content="_Test"  />

Then press Alt and you will see the button text change to have the letter T underscored. Then if you press T the button will be pressed. This is accessibility in action.

The brute-force solution

The simplest and quickest way to fix your code is to just disable accessiblity by marking the Alt key as Handled every time it is pressed via the KeyDown event or PreviewKeyDown event of the Window.

NOTE: This will disable all accessibility in your WPF window and I would not recommend it.

WPF Window

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        KeyDown="Window_KeyDown"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <ScrollViewer x:Name="ScrollViewer">
            <Border Background="Blue" Height="5000"></Border>
        </ScrollViewer>
    </Grid>
</Window>

Code Behind

using System.Windows;
using System.Windows.Input;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.System && (Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt)))
                e.Handled = true;
        }
    }
}

Note that we basically hook into the key down event of the window to capture all keys, then we listen out if the key is a System key (which could include function keys like F1, F2 etc...) and if so they we check if either Alt key is pressed.

We do the first check to speed up the logic a little and not checking for Alt keys unless we know it is a System key first. Otherwise every key you press on your entire keyboard such as typing in a text box would involve the check for both Alt keys and slow things down.

The intelligent solution

In order to achieve your desired effect, really you need to be able to detect if your user is in your "mode" or "focus" where when the user begins to press Alt they are in the mode that you wish to disable the Access Keys.

A simple way to determine this mode would be for example only disable Alt when the scroll viewer is focused. Then your logic would change to have one additional check in it like so.

private void Window_KeyDown(object sender, KeyEventArgs e)
{
    if (e.Key == Key.System && (Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt)) 
        && e.OriginalSource == ScrollViewer) // <-- Added this check
        e.Handled = true;
}

Then you will only disable the Alt when the view has focus (has been clicked into for example).

Your challenge becomes harder if you want to detect the mode (to disable Alt action) after the Alt key has already been pressed, such as detecting it once scrolling starts. The reason for that is as soon as Alt is pressed the access key behaviour starts (on key down) so then removing that behaviour after the fact is more difficult.

The dubious solution

So after writing the above I figured it's best to give you an example of after-alt style detection. So here it is. This is only a slightly thought out and tested example and may well be buggy (mainly the Task.Delay(0) relying on the processing of previously released System Key message before focusing) but seems to work ok.

Now you can press Alt as normal, as well as press Alt then start scrolling, and after release the access keys are defocused.

WPF Window

<Window x:Class="WpfApp1.MainWindow"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                mc:Ignorable="d"
                KeyDown="Window_KeyDown"
                KeyUp="Window_KeyUp"
                Title="MainWindow" Height="450" Width="800">

    <DockPanel>
        <Menu DockPanel.Dock="Top">
            <MenuItem Header="_File">
                <MenuItem Header="_Open"/>
                <MenuItem Header="_Close"/>
                <MenuItem Header="_Save"/>
            </MenuItem>
        </Menu>

        <ScrollViewer x:Name="ScrollViewer" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel">
            <Border Background="Blue" Height="5000"></Border>
        </ScrollViewer>
    </DockPanel>
</Window>

Code Behind

using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;

namespace WpfApp1
{

    public partial class MainWindow : Window
    {
        private bool mHasScrolledWithSystemKeyDown = false;
        private bool mSystemKeyIsDown = false;
        private IInputElement mLastFocusedControl;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_KeyDown(object sender, KeyEventArgs e)
        {
            // If the system key is down...
            if (e.Key == Key.System)
                // Track it
                mSystemKeyIsDown = true;
        }

        private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
        {
            // If the system key is down while scrolling...
            if (mSystemKeyIsDown)
            {
                // If it is the first scroll since it being down...
                if (!mHasScrolledWithSystemKeyDown)
                    // Remember currently focused item
                    mLastFocusedControl = Keyboard.FocusedElement;

                // And flag as scrolled with system key down
                mHasScrolledWithSystemKeyDown = true;

                // Prevent scroll...
                e.Handled = true;

                // TODO: Zoom
            }
        }

        private void Window_KeyUp(object sender, KeyEventArgs e)
        {
            // On system key up...
            if (e.Key == Key.System)
            {
                // Track it
                mSystemKeyIsDown = false;

                // If we had scrolled with the system key down...
                if (mHasScrolledWithSystemKeyDown)
                    // Cause a small delay to allow this key up to process
                    Task.Delay(0).ContinueWith((t) => Dispatcher.Invoke(() =>
                    {
                        // Then focus the last control to "close" the system menu gracefully
                        mLastFocusedControl?.Focus();
                    }));

                // Flag the has scrolled to false to start again
                mHasScrolledWithSystemKeyDown = false;
            }
        }
    }
}

On Closing

Just a note that if you pursue a method of trying to detect after Alt is pressed to then negate the action, be careful if using any message pump commands like SendMessage as simply sending an VM_KEYUP command for Alt will not work as the button that is physically pressed over and over while it is held, and there are many quirks with the message pump too that you really need to know what you are doing to not break other things.

angelsix
  • 392
  • 1
  • 7
  • Welcome to Stack Overflow! Congrats on a thorough first answer. – Mike Strobel Mar 29 '18 at 10:32
  • If I understood correctly **Alt** key is not really your regular key (that explains `if (e.Key == Key.LeftAlt)` doesn't work as e.Key contains "System"). So If I really wanted to implement Alt+Mousewheel, where will be no easy way. It is my first time to hear the SendMessage, I'll look up on it. – Sectoid Mar 29 '18 at 11:59
  • The system (Windows) intercepts the Alt and sends in a System key state to the message pump of the window. However for what you needmy dubious solution should work fine, even if it needs some refinement later if you find issues. I tested it and it fully works – angelsix Mar 30 '18 at 07:54
  • Tested. Saved me from introducing some even more dubious, complex codes which I really don't understand/will not ever be understood. Nice work tho, very clean and understandable(and informative). – Sectoid Mar 30 '18 at 13:22