As Paul has left, I have to use another way of typing: Serenade. It’s actually a pretty cool speech-to-code tool, but it definitely takes some time to get used to it. For practicing, I’m adding a few event handlers to the WinUI 3 app.
(Sometimes I have the feeling that I’m not doing that great when I say “Undo”, then repeat “Undo! Undo!!!” another ten times, and afterwards Hubert adds: “delete Arthur”.)
TLDR; Issues addressed
- Set focus on text field when WinUI 3 window appears
- Hide WinUI 3 window on losing focus
- Trigger Start button action on pressing Enter
- Delete content of text box on pressing Escape
Handling activated and deactivated event of the WinUI 3 window
One of the features that we want to add is that after you press the hot key to open up the window, you can immediately start typing the task you are working on, and then press enter to start tracking the time. For that, the focus has to jump to the input box.
In WinUI 3 all UIElements implement the Focus function. We just have to assign a name to our text box field, so we can refer to it in the code:
<TextBox x:Name="input" />
Then we have to find the proper place to call the Focus function:
input.Focus(FocusState.Programmatic);
The other use case that we would like to handle is when the user clicks outside of the window. In this case we assume that they probably changed their mind and don’t want to record any times right now, or just quickly checked whether or not the timer is running on the proper task. To hide the window, we have already found a solution in Win32 in the global hot key chapter. In a nutshell:
- the CsWin32 nuget package has to be added as a dependency,
ShowWindowfunction has to be added in theNativeMethods.txtfile,- the window handle has to be stored for the
MainWindow, - then we have to call the
PInvoke.ShowWindowfunction with our window handle and theSW_HIDEparameter.
Our MainWindow is a subclass of the Window class, which has an Activated event. This is sent both when the window becomes active and when it becomes inactive. You can differentiate based on the second argument of the event handler:
public MainWindow()
{
// ... rest of the constructor
Activated += OnActivated;
}
private void OnActivated(object sender, WindowActivatedEventArgs eventArgs)
{
if (eventArgs.WindowActivationState == WindowActivationState.Deactivated)
{
// window was deactivated
}
else
{
// window was activated
}
}
Based on this code snippet, this is how it looks like when we add the focus on the input field when the window appears, and the hide action when the window loses focus:
public sealed partial class MainWindow : Window
{
private readonly Windows.Win32.Foundation.HWND hwnd;
public MainWindow()
{
// ... rest of the constructor
hwnd = new Windows.Win32.Foundation.HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
Activated += OnActivated;
}
private void OnActivated(object sender, WindowActivatedEventArgs eventArgs)
{
if (eventArgs.WindowActivationState == WindowActivationState.Deactivated)
{
PInvoke.ShowWindow(hwnd, Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE);
}
else
{
input.Focus(FocusState.Programmatic);
}
}
// ... rest of the class
}
Note that in the hot key chapter we have set the focus on the window when the hot key is pressed (PInvoke.SetFocus(hwnd)). To be able to properly set the focus on the text box you have to delete that line from the hot key procedure.
Handling key events of a text box in a WinUI 3 application
The next feature that we have to address is that when the user finished typing their task, they press Enter to start tracking the time. We also provide a Start button for the same functionality for those users who prefer clicking.
For the button we can use the usual Click event where the event handler gets a sender (as an object) and a RoutedEventArgs argument. The event handler can be attached in the XAML file:
<Button Click="StartButton_Click">Start</Button>
To handle pressing Enter in the TextBox, we can use KeyboardAccelerators. In this we have to define a key, and an event handler function where the event handler gets a sender (as a KeyboardAccelerator) and a KeyboardAcceleratorInvokedEventArgs argument. It looks like this on the XAML side:
<TextBox x:Name="input">
<TextBox.KeyboardAccelerators>
<KeyboardAccelerator Key="Enter" Invoked="InputBox_EnterKey_Invoked" />
</TextBox.KeyboardAccelerators>
</TextBox>
Unfortunately as the event handler signatures are quite different, we can’t call the StartButton_Click function directly, but of course we can delegate to a common method in the event handlers.
private void StartButton_Click(object sender, RoutedEventArgs eventArgs)
{
StartTracking();
}
private void InputBox_EnterKey_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs eventArgs)
{
StartTracking();
}
private void StartTracking()
{
Debug.WriteLine("Tracking started");
}
While running the application, you might notice that every time you press Enter, the debug message is logged twice. This seems to be a bug in the framework, but luckily there is a workaround: you can set that your eventArgs were already handled.
private void InputBox_EnterKey_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs eventArgs)
{
StartTracking();
eventArgs.Handled = true;
}
Now based on our current code it’s no challenge to implement the next feature: if the user presses the Escape key, we will clear the input field. For this we need one more KeyboardAccelerator which handles the Escape key and an event handler that sets the content of the input element empty. Here is the XAML part with the extended keyboard accelerators:
<TextBox x:Name="input">
<TextBox.KeyboardAccelerators>
<KeyboardAccelerator Key="Enter" Invoked="InputBox_EnterKey_Invoked" />
<KeyboardAccelerator Key="Escape" Invoked="InputBox_EscapeKey_Invoked" />
</TextBox.KeyboardAccelerators>
</TextBox>
And this is the event handler, which also addresses the issue of firing the key event twice:
private void InputBox_EscapeKey_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs eventArgs)
{
if (input.Text != string.Empty)
{
input.Text = string.Empty;
}
else
{
PInvoke.ShowWindow(hwnd, Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE);
}
eventArgs.Handled = true;
}