Being a technology for developing rich user interfaces, it makes sense that WPF should provide good support for integrating video and audio into your applications and for getting input from the user. This chapter takes a look at some of the video, audio, and user input capabilities provided by WPF.
As with so many things, WPF makes it relatively easy to add strong multimedia and user input support to your applications. The recipes in this chapter describe how to:
Play standard Windows system sounds (recipe 12-1)
Play sounds when users interact with controls (recipe 12-2)
Play and control the playback characteristics of media files (recipes 12-3)
Respond when the user clicks controls with the mouse (recipes 12-4 and 12-5)
Respond when the user moves the mouse wheel (recipe 12-6)
Handle drag and drop operations between controls (recipe 12-7)
Handle keyboard events (recipe 12-8)
Query the state of the keyboard (recipe 12-9)
Suppress mouse and keyboard events (recipe 12-10)
Use the static properties of the System.Media.SystemSounds
class to obtain a System.Media.SystemSound
object representing the sound you want to play
. Then call the Play method on the SystemSound
object.
The SystemSounds
class provides a simple way of playing some of the most commonly used standard Windows system sounds. The SystemSounds
class implements five static properties: Asterisk
, Beep
, Exclamation
, Hand
, and Question
. Each of these properties returns a SystemSound
object representing a particular sound. Once you have the appropriate SystemSound
object, simply call its Play
method to play the sound.
The sound played by each SystemSound
object depends on the user's Windows configuration on the Sounds tab of the Sounds and Audio Devices control panel. If the user has no sound associated with the specific type of event, calling Play
on the related SystemSound
object will make no sound. You have no control over any aspect of the sound playback such as volume or duration.
The following XAML displays five buttons (see Figure 12-1). Each button is configured to play a different SystemSound
using the SystemSounds class in the code-behind:
<Window x:Class="Recipe_12_01.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 12_01" Height="120" Width="300"> <Canvas> <Canvas.Resources> <!-- Style all buttons the same --> <Style TargetType="{x:Type Button}"> <Setter Property="Height" Value="25" /> <Setter Property="MinWidth" Value="70" /> <EventSetter Event="Click" Handler="Button_Click" /> </Style> </Canvas.Resources> <Button Canvas.Top="15" Canvas.Left="30" Content="Asterisk" Name="btnAsterisk" /> <Button Canvas.Top="15" Canvas.Left="110" Content="Beep" Name="btnBeep" /> <Button Canvas.Top="15" Canvas.Left="190" Content="Exclamation" Name="btnExclamation" /> <Button Canvas.Top="50" Canvas.Left="70" Content="Hand" Name="btnHand" /> <Button Canvas.Top="50" Canvas.Left="150" Content="Question" Name="btnQuestion" /> </Canvas> </Window>
The following code-behind determines which button the user has clicked and plays the appropriate sound:
using System.Windows; using System.Windows.Controls; namespace Recipe_12_01 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Handles the click events for all system sound buttons. private void Button_Click(object sender, RoutedEventArgs e) { Button btn = sender as Button; if (btn != null) { // Simple switch on the name of the button. switch (btn.Content.ToString()) { case "Asterisk": System.Media.SystemSounds.Asterisk.Play(); break; case "Beep": System.Media.SystemSounds.Beep.Play(); break; case "Exclamation": System.Media.SystemSounds.Exclamation.Play(); break; case "Hand": System.Media.SystemSounds.Hand.Play(); break; case "Question": System.Media.SystemSounds.Question.Play(); break;
default: string msg = "Sound not implemented: " + btn.Content; MessageBox.Show(msg); break; } } } } }
You need to play a sound when the user interacts with a control, such as clicking a button or moving a slider.
Declare a System.Windows.Controls.MediaElement
on your form. Configure an EventTrigger
on the control, and use a StoryBoard
containing a System.Windows.Media.MediaTimeline
to play the desired audio through the MediaElement
in response to the appropriate event.
An EventTrigger
hooks the event specified in its RoutedEvent
property. When the event fires, the EventTrigger
applies the animation specified in its Actions
property. As the action, you can configure the animation to play a media file using a MediaTimeline
.
You can define an EventTrigger
directly on a control by declaring it in the Triggers
collection of the control. In the RoutedEvent
property of the EventTrigger
, specify the name of the event you want to trigger the sound, for example, Button.Click
. Within the Actions
element of the Triggers
collection, declare a BeginStoryboard
element containing a Storyboard
element. In the Storyboard
element, you declare the MediaTimeline
.
You specify the media file to play using the Source
property of the MediaTimeline
and the MediaElement
that will actually do the playback in the Storyboard.TargetName
property. When the user interacts with the control, the specified sound will play back asynchronously.
Chapter 6 provides more details on the use of triggers, and Chapter 11 provides extensive coverage of animation in WPF.
The following XAML demonstrates how to assign an EventTrigger
to the Click
event of a System.Windows.Controls.Button
and the ValueChanged
event of a System.Windows.Controls. Slider.
Figure 12-2 shows the example running.
<Window x:Class="Recipe_12_02.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 12_02" Height="100" Width="300"> <StackPanel> <!-- MediaElement for sond playback. --> <MediaElement Name="meMediaElem" /> <!-- The Button that goes Ding! --> <UniformGrid Height="70" Columns="2"> <Button Content="Ding" MaxHeight="25" MaxWidth="70"> <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <MediaTimeline Source="ding.wav" Storyboard.TargetName="meMediaElem"/> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </Button.Triggers> </Button> <!-- The Slider that goes Ring! --> <Slider MaxHeight="25" MaxWidth="100" > <Slider.Triggers> <EventTrigger RoutedEvent="Slider.ValueChanged"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <MediaTimeline
Source="ringin.wav" Storyboard.TargetName="meMediaElem" /> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </Slider.Triggers> </Slider> </UniformGrid> </StackPanel> </Window>
You need to play a sound or music file and allow the user to control the progress of the playback, volume, or balance.
Use a System.Windows.Controls.MediaElement
to handle the playback of the media file. Use a System.Windows.Media.MediaTimeline
to control the playback of the desired media through the MediaElement
. Declare the set of controls that will enable the user to control the playback and associate triggers with these controls that start, stop, pause, and resume the animation controlling the MediaTimeline
. For volume and balance, data bind controls to the Volume1
and Balance
properties of the MediaElement
.
A MediaElement
performs the playback of a media file, and you control that playback via animation using a MediaTimeline
. To control the playback, you use a set of EventTrigger
elements to start, stop, pause, and resume the animation Storyboard containing the MediaTimeline
.
Either you can define the EventTrigger
elements in the Triggers
collection on the controls that control the playback or you can centralize their declaration by placing them on the container in which you place the controls. Within the Actions
element of the Triggers
collection, declare the Storyboard
elements to control the MediaTimeline
.
Chapter 5 provides more details on using data binding, Chapter 6 discusses using triggers in more detail, and Chapter 11 provides extensive coverage of animation in WPF.
One complexity arises when you want a control, such as a System.Windows.Controls.Slider
, to show the current position within the media file as well as allow the user to change the current play position. To update the display of the current play position, you must attach an event handler to the MediaTimeline.CurrentTimeInvalidated
event, which updates the Slider
position when it fires.
To move the play position in response to the Slider
position changing, you attach an event handler to the Slider.ValueChanged
property, which calls the Stoyboard.Seek
method to change the current MediaTimeline
play position. However, you must include logic in the event handlers to stop these events from triggering each other repeatedly as the user and MediaTimeline
try to update the Slider
position (and in turn the media play position) at the same time.
The following XAML demonstrates how to play an AVI file using a MediaElement
and allow the user to start, stop, pause, and resume the playback. The user can also move quickly back and forth through the media file using a slider to position the current play position, as well as control the volume and balance of the audio (see Figure 12-3).
<Window x:Class="Recipe_12_03.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 12_03" Height="450" Width="300"> <StackPanel x:Name="Panel"> <StackPanel.Resources> <!-- Style all buttons the same. --> <Style TargetType="{x:Type Button}"> <Setter Property="Height" Value="25" /> <Setter Property="MinWidth" Value="50" /> </Style> </StackPanel.Resources> <StackPanel.Triggers> <!-- Triggers for handling playback of media file. --> <EventTrigger RoutedEvent="Button.Click" SourceName="btnPlay"> <EventTrigger.Actions> <BeginStoryboard Name="ClockStoryboard"> <Storyboard x:Name="Storyboard" SlipBehavior="Slip" CurrentTimeInvalidated="Storyboard_Changed"> <MediaTimeline BeginTime="0" Source="clock.avi" Storyboard.TargetName="meMediaElement" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard>
</EventTrigger.Actions> </EventTrigger> <EventTrigger RoutedEvent="Button.Click" SourceName="btnPause"> <EventTrigger.Actions> <PauseStoryboard BeginStoryboardName="ClockStoryboard" /> </EventTrigger.Actions> </EventTrigger> <EventTrigger RoutedEvent="Button.Click" SourceName="btnResume"> <EventTrigger.Actions> <ResumeStoryboard BeginStoryboardName="ClockStoryboard" /> </EventTrigger.Actions> </EventTrigger> <EventTrigger RoutedEvent="Button.Click" SourceName="btnStop"> <EventTrigger.Actions> <StopStoryboard BeginStoryboardName="ClockStoryboard" /> </EventTrigger.Actions> </EventTrigger> <EventTrigger RoutedEvent="Slider.PreviewMouseLeftButtonDown" SourceName="sldPosition" > <PauseStoryboard BeginStoryboardName="ClockStoryboard" /> </EventTrigger> <EventTrigger RoutedEvent="Slider.PreviewMouseLeftButtonUp" SourceName="sldPosition" > <ResumeStoryboard BeginStoryboardName="ClockStoryboard" /> </EventTrigger> </StackPanel.Triggers> <!-- Media element to play the sound, music, or video file. --> <MediaElement Name="meMediaElement" HorizontalAlignment="Center" Margin="5" MinHeight="300" Stretch="Fill" MediaOpened="MediaOpened" /> <!-- Button controls for play, pause, resume, and stop. --> <StackPanel HorizontalAlignment="Center" Orientation="Horizontal"> <Button Content="_Play" Name="btnPlay" /> <Button Content="P_ause" Name="btnPause" /> <Button Content="_Resume" Name="btnResume" /> <Button Content="_Stop" Name="btnStop" /> </StackPanel> <!-- Slider shows the position within the media. --> <Slider HorizontalAlignment="Center" Margin="5" Name="sldPosition" Width="250" ValueChanged="sldPosition_ValueChanged"> </Slider> <!-- Sliders to control volume and balance. --> <Grid>
<Grid.ColumnDefinitions> <ColumnDefinition Width="1*"/> <ColumnDefinition Width="4*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <TextBlock Grid.Column="0" Grid.Row="0" Text="Volume:" HorizontalAlignment="Right" VerticalAlignment="Center"/> <Slider Grid.Column="1" Grid.Row="0" Minimum="0" Maximum="1" TickFrequency="0.1" TickPlacement="TopLeft" Value="{Binding ElementName=meMediaElement, Path=Volume, Mode=TwoWay}" /> <TextBlock Grid.Column="0" Grid.Row="1" Text="Balance:" HorizontalAlignment="Right" VerticalAlignment="Center"/> <Slider Grid.Column="1" Grid.Row="1" Minimum="-1" Maximum="1" TickFrequency="0.2" TickPlacement="TopLeft" Value="{Binding ElementName=meMediaElement, Path=Balance, Mode=TwoWay}" /> </Grid> </StackPanel> </Window>
The following code-behind shows the event handlers that allow the user to set the current play position using a slider and update the position of the slider to reflect the current play position:
using System; using System.Windows; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; namespace Recipe_12_03 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { bool ignoreValueChanged = false; public Window1() { InitializeComponent(); }
// Handles the opening of the media file and sets the Maximum // value of the position slider based on the natural duration // of the media file. private void MediaOpened(object sender, EventArgs e) { sldPosition.Maximum = meMediaElement.NaturalDuration.TimeSpan.TotalMilliseconds; } // Updates the position slider when the media time changes. private void Storyboard_Changed(object sender, EventArgs e) { ClockGroup clockGroup = sender as ClockGroup; MediaClock mediaClock = clockGroup.Children[0] as MediaClock; if (mediaClock.CurrentProgress.HasValue) { ignoreValueChanged = true; sldPosition.Value = meMediaElement.Position.TotalMilliseconds; ignoreValueChanged = false; } } // Handles the movement of the slider and updates the position // being played. private void sldPosition_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { if (ignoreValueChanged) { return; } Storyboard.Seek(Panel, TimeSpan.FromMilliseconds(sldPosition.Value), TimeSeekOrigin.BeginTime); } } }
Handle the MouseDown
or MouseUp
event inherited from System.Windows.UIElement
, the MouseDoubleClick
event inherited from System.Windows.Control
, or the Click
event inherited from System.Windows.Control.ButtonBase
.
Depending on the UI element you are working with and the kind of functionality you are trying to implement, you can handle mouse click events in a variety of ways. The MouseDown
or MouseUp
events are the most widely available because they are implemented by UIElement
. The MouseDown
event occurs as soon as the user clicks any mouse button while over a UIElement
, but the MouseUp
event occurs only when the user releases the button. The MouseDoubleClick
event implemented by Control
is raised when the user double-clicks a Control
.
The UIElement
class also implements the MouseLeftButtonDown
, MouseLeftButtonUp
, MouseRightButtonDown
, and MouseRightButtonUp
, which as the names suggest allow you to be selective about which mouse button causes an event to be raised.
The ButtonBase
class provides a special Click
event support, which overrides the basic behavior of the MouseLeftButtonDown
event implemented by UIElement
.
The following XAML demonstrates how to hook various mouse click event handlers to a variety of control types including a Button
, Label
, and TextBlock
(from the System.Windows.Controls
namespace) as well as a System.Windows.Shapes.Rectangle
:
<Window x:Class="Recipe_12_04.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 12_04" Height="150" Width="300"> <UniformGrid Columns="2" Rows="2"> <Button Content="Click" Click="Button_Click" MaxHeight="25" MaxWidth="100" /> <Label Background="LightBlue" Content="Double Click" HorizontalContentAlignment="Center" MaxHeight="25" MaxWidth="100" MouseDoubleClick="Label_MouseDoubleClick" /> <TextBlock Background="Turquoise" Padding="25,7" Text="Mouse Up" MouseUp="TextBlock_MouseUp" HorizontalAlignment="Center" VerticalAlignment="Center"/> <Canvas> <Rectangle Canvas.Top="15" Canvas.Left="20" Height="25" Width="100" Fill="Aqua" MouseDown="Rectangle_MouseDown" /> <TextBlock Canvas.Top="20" Canvas.Left="40" Text="Mouse Down" IsHitTestVisible="False"/> </Canvas> </UniformGrid> </Window>
The following code-behind shows the simple event handler implementations for the various mouse click events:
using System; using System.Windows; using System.Windows.Input; namespace Recipe_12_04 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Handles the Click event on the Button. private void Button_Click(object sender, RoutedEventArgs e) { MessageBox.Show("Mouse Click", "Button"); } // Handles the MouseDoubleClick event on the Label. private void Label_MouseDoubleClick(object sender, MouseButtonEventArgs e) { MessageBox.Show("Mouse Double Click", "Label"); } // Handles the MouseDown event on the Rectangle. private void Rectangle_MouseDown(object sender, MouseButtonEventArgs e) { MessageBox.Show("Mouse Down", "Rectangle"); } // Handles the MouseUp event on the TextBlock. private void TextBlock_MouseUp(object sender, MouseButtonEventArgs e) { MessageBox.Show("Mouse Up", "TextBlock"); } } }
Figure 12-4 shows the resulting window.
Handle the System.Windows.UIElement.MouseUp, System.Windows.UIElement.MouseDown
, or System.Windows.Control.ButtonBase.Click
event in the container of the controls.
WPF automatically bubbles the MouseDown
, MouseUp
, and Click
events up the containment hierarchy, making it a trivial exercise to handle these events at the container level instead of that of the individual controls. All you need to do is declare an event handler of the appropriate type at the container instead of the individual control. If the container does not support the event you want to handle—such as the Click
event, which is implemented by ButtonBase
—you use the attached event syntax Buttonbase.Click
as the event name to ensure the correct event is handled.
If the control is nested within a number of containers, the bubbled events are automatically routed up through all container levels, so you can handle the event at one or more containers where appropriate.
The following XAML demonstrates how to handle control events at the container level. To demonstrate the bubbling of events through multiple containers, when the user clicks the Rectangle
in the bottom-right corner (see Figure 12-5), the event is first handled by the Canvas
and then by the UniformGrid
, because both containers handle the MouseDown
event.
<Window x:Class="Recipe_12_05.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 12_05" Height="150" Width="300"> <UniformGrid Columns="2" Rows="2" ButtonBase.Click="UniformGrid_Click" MouseDown="UniformGrid_MouseDown"> <Button Content="Button" MaxHeight="25" MaxWidth="70" Name="Button"/> <Label Background="LightBlue" Content="Label" Name="Label" HorizontalContentAlignment="Center" MaxHeight="25" MaxWidth="100"/> <TextBlock Background="Turquoise" Padding="25,7" Text="TextBlock" HorizontalAlignment="Center" VerticalAlignment="Center" Name="TextBlock"/> <Canvas MouseDown="Canvas_MouseDown"> <Rectangle Canvas.Top="15" Canvas.Left="20" Fill="Aqua" Height="25" Width="100" Name="Rectangle"/> <TextBlock Canvas.Top="20" Canvas.Left="45" Text="Rectangle" IsHitTestVisible="False"/> </Canvas> </UniformGrid> </Window>
The following code-behind contains the event-handling code for the Canvas
and UniformGrid
controls:
using System.Windows; using System.Windows.Input; namespace Recipe_12_05 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Handles the MouseDown event on the Canvas. private void Canvas_MouseDown(object sender, MouseButtonEventArgs e)
{ FrameworkElement fe = e.OriginalSource as FrameworkElement; MessageBox.Show("Mouse Down on " + fe.Name, "Canvas"); } // Handles the Click event on the UniformGrid. private void UniformGrid_Click(object sender, RoutedEventArgs e) { FrameworkElement fe = e.OriginalSource as FrameworkElement; MessageBox.Show("Mouse Click on " + fe.Name, "Uniform Grid"); } // Handles the MouseDown event on the UniformGrid. private void UniformGrid_MouseDown(object sender, MouseButtonEventArgs e) { FrameworkElement fe = e.OriginalSource as FrameworkElement; MessageBox.Show("Mouse Down on " + fe.Name, "Uniform Grid"); } } }
On the control you want to respond to the mouse wheel, handle the MouseWheel
event inherited from the System.Windows.UIElement
class.
When the mouse pointer is over an element and the user moves the mouse wheel, a MouseWheel
event is raised on the element. To handle these events, simply attach an event handler to the MouseWheel
event.
When WPF calls the event handler, it passes the handler a System.Windows.Input.MouseWheelEventArgs
object that describes the mouse wheel event and the state of the mouse buttons. The Delta
property of the MouseWheelEventArgs
is positive if the mouse wheel is moved away from the user and negative if the mouse wheel is moved toward the user. The LeftButton
and RightButton
properties indicate whether the buttons are currently pressed or released using values of the System.Windows.Input.MouseButtonState
enumeration.
The following XAML demonstrates how to attach mouse wheel event handlers to various UI elements. The application (shown in Figure 12-6) contains a Slider
, a RichTextBox
, and a Rectangle
that each responds to the mouse wheel when the mouse pointer is over the element. The RichTextBox
already has mouse wheel support built in to perform vertical scrolling of the content. The Slider
uses a MouseWheel
event handler to move the slider thumb left and right. The Rectangle
uses a MouseWheel
event handler to enlarge and decrease its Height
and Width
properties depending on whether the left mouse button is currently pressed.
<Window x:Class="Recipe_12_06.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 12_06" Height="300" Width="300"> <Canvas> <Slider Canvas.Top="10" Canvas.Left="20" Name="sldSlider" Minimum="0" Maximum="1000" Value="500" Width="250" MouseWheel="Slider_MouseWheel"/> <RichTextBox Canvas.Top="50" Canvas.Left="20" Width="250" Height="100" VerticalScrollBarVisibility="Visible"> <FlowDocument> <Paragraph FontSize="12">
Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. </Paragraph> <Paragraph FontSize="15"> Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure. </Paragraph> <Paragraph FontSize="18">A List</Paragraph> <List> <ListItem> <Paragraph> <Bold>Bold List Item</Bold> </Paragraph> </ListItem> <ListItem> <Paragraph> <Italic>Italic List Item</Italic> </Paragraph> </ListItem> <ListItem> <Paragraph> <Underline>Underlined List Item</Underline> </Paragraph> </ListItem> </List> </FlowDocument> </RichTextBox> <Rectangle Canvas.Top="160" Canvas.Left="20" Name="shpRectangle" Fill="LightBlue" Width="50" Height="50" MouseWheel="Rectangle_MouseWheel"> </Rectangle> </Canvas> </Window>
The following code-behind shows the event handlers that handle the MouseWheel
event for the Slider
and Rectangle
:
using System.Windows; using System.Windows.Input; namespace Recipe_12_06 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window
{ public Window1() { InitializeComponent(); } // Handles the MouseWheel event on the Slider. private void Slider_MouseWheel(object sender, MouseWheelEventArgs e) { // Increment or decrement the slider position depending on // whether the wheel was moved up or down. sldSlider.Value += (e.Delta > 0) ? 5 : −5; } // Handles the MouseWheel event on the Rectangle. private void Rectangle_MouseWheel(object sender, MouseWheelEventArgs e) { if (e.LeftButton == MouseButtonState.Pressed) { // If the left button is pressed, increment or // decrement the width. double newWidth = shpRectangle.Width += (e.Delta > 0) ? 5 : −5; if (newWidth < 10) newWidth = 10; if (newWidth > 200) newWidth = 200; shpRectangle.Width = newWidth; } else { // If the left button is not pressed, increment or // decrement the height. double newHeight = shpRectangle.Height += (e.Delta > 0) ? 5 : −5; if (newHeight < 10) newHeight = 10; if (newHeight > 200) newHeight = 200; shpRectangle.Height = newHeight; } } } }
You need to allow the user to drag items from a System.Windows.Controls.ListBox
to a System.Windows.Controls.Canvas
.
Drag and drop is relatively simple to implement in WPF but contains a lot of variations depending on what you are trying to do and what content you are dragging. This example focuses on dragging content from a ListBox
to a Canvas
, but the principles are similar for other types of drag and drop operations and can be adapted easily.
On the ListBox
or ListBoxItem
, handle the PreviewMouseLeftButtonDown
event to identify the start of a possible drag operation and identify the ListBoxItem
being dragged. Handle the PreviewMouseMove
event to determine whether the user is actually dragging the item, and if so, set up the drop operation using the static System.Windows.DragDrop
class. On the Canvas
(the target for the drop operation), handle the DragEnter
and Drop
events to support the dropping of dragged content.
The static DragDrop
class provides the functionality central to making it easy to execute drag and drop operations in WPF. First, however, you must determine that the user is actually trying to drag something.
There is no single best way to do this, but usually you will need a combination of handling MouseLeftButtonDown
or PreviewMouseLeftButtonDown
events to know when the user clicks something and MouseMove
or PreviewMouseMove
events to determine whether the user is moving the mouse while holding the left button down. Also, you should use the SystemParameters.MinimumHorizontalDragDistance
and SystemParameters.MinimumVerticalDragDistance
properties to make sure the user has dragged the item a sufficient distance to be considered a drag operation; otherwise, the user will often get false drag operations starting as they click items.
Once you are sure the user is trying to drag something, you configure the DragDrop
object using the DoDragDrop
method. You must pass the DoDragDrop
method a reference to the source object being dragged, a System.Object
containing the data that the drag operation is taking with it, and a value from the System.Windows.DragDropEffects
enumeration representing the type of drag operation being performed. Commonly used values of the DragDropEffects
enumeration are Copy, Move
, and Link
. The type of operation is often driven by special keys being held down at the time of clicking, for example, holding the Control key signals the user's intent to copy (see recipe 12-9 for information on how to query keyboard state).
On the target of the drop operation, implement event handlers for the DragEnter
and Drop
events. The DragEnter
handler allows you to control the behavior seen by the user as the mouse pointer enters the target control. This usually indicates whether the control is a suitable target for the type of content the user is dragging. The Drop
event signals that the user has released the left mouse button and indicates that the content contained in the DragDrop
object should be retrieved (using the Data.GetData
method of the DragEventArgs
object passed to the Drop
event handler) and inserted into the target control.
The following XAML demonstrates how to set up a ListBox
with ListBoxItem
objects that support drag and drop operations (see Figure 12-7):
<Window x:Class="Recipe_12_07.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 12_07" Height="300" Width="300"> <DockPanel LastChildFill="True" > <ListBox DockPanel.Dock="Left" Name="lstLabels"> <ListBox.Resources> <Style TargetType="{x:Type ListBoxItem}"> <Setter Property="FontSize" Value="14" /> <Setter Property="Margin" Value="2" /> <EventSetter Event="PreviewMouseLeftButtonDown" Handler="ListBoxItem_PreviewMouseLeftButtonDown"/> <EventSetter Event="PreviewMouseMove" Handler="ListBoxItem_PreviewMouseMove"/> </Style>
</ListBox.Resources> <ListBoxItem IsSelected="True">Allen</ListBoxItem> <ListBoxItem>Andy</ListBoxItem> <ListBoxItem>Antoan</ListBoxItem> <ListBoxItem>Bruce</ListBoxItem> <ListBoxItem>Ian</ListBoxItem> <ListBoxItem>Matthew</ListBoxItem> <ListBoxItem>Sam</ListBoxItem> <ListBoxItem>Simon</ListBoxItem> </ListBox> <Canvas AllowDrop="True" Background="Transparent" DragEnter="cvsSurface_DragEnter" Drop="cvsSurface_Drop" Name="cvsSurface" > </Canvas> </DockPanel> </Window>
The following code-behind contains the event handlers that allow the example to identify the ListBoxItem
that the user is dragging, determine whether a mouse movement constitutes a drag operation, and allow the Canvas
to receive the dragged ListBoxItem
content.
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace Recipe_12_07 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { private ListBoxItem draggedItem; private Point startDragPoint; public Window1() { InitializeComponent(); } // Handles the DragEnter event for the Canvas. Changes the mouse // pointer to show the user that copy is an option if the drop // text content is over the Canvas. private void cvsSurface_DragEnter(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.Text))
{ e.Effects = DragDropEffects.Copy; } else { e.Effects = DragDropEffects.None; } } // Handles the Drop event for the Canvas. Creates a new Label // and adds it to the Canvas at the location of the mouse pointer. private void cvsSurface_Drop(object sender, DragEventArgs e) { // Create a new Label. Label newLabel = new Label(); newLabel.Content = e.Data.GetData(DataFormats.Text); newLabel.FontSize = 14; // Add the Label to the Canvas and position it. cvsSurface.Children.Add(newLabel); Canvas.SetLeft(newLabel, e.GetPosition(cvsSurface).X); Canvas.SetTop(newLabel, e.GetPosition(cvsSurface).Y); } // Handles the PreviewMouseLeftButtonDown event for all ListBoxItem // objects. Stores a reference to the item being dragged and the // point at which the drag started. private void ListBoxItem_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { draggedItem = sender as ListBoxItem; startDragPoint = e.GetPosition(null); } // Handles the PreviewMouseMove event for all ListBoxItem objects. // Determines whether the mouse has been moved far enough to be // considered a drag operation. private void ListBoxItem_PreviewMouseMove(object sender, MouseEventArgs e) { if (e.LeftButton == MouseButtonState.Pressed) { Point position = e.GetPosition(null); if (Math.Abs(position.X - startDragPoint.X) > SystemParameters.MinimumHorizontalDragDistance || Math.Abs(position.Y - startDragPoint.Y) > SystemParameters.MinimumVerticalDragDistance)
{ // User is dragging, set up the DragDrop behavior. DragDrop.DoDragDrop(draggedItem, draggedItem.Content, DragDropEffects.Copy); } } } } }
To take an action when the user presses a key, handle the PreviewKeyDown
or KeyDown
event. To take an action when the user releases a key, handle the PreviewKeyUp
or KeyUp
event. To take an action as the target element receives the text input, handle the PreviewTextInput
or TextInput
event.
When the user presses a key, WPF fires the following sequence of events:
PreviewKeyDown
KeyDown
PreviewTextInput
TextInput
PreviewKeyUp
KeyUp
The events that begin with Preview
are tunneling events that go top down through the container hierarchy to the target control. The other events are bubbling events that go from the target control up through the container hierarchy. This sequence of events going both up and down the container hierarchy provides a great deal of flexibility as to when and where you want to handle keyboard events.
The KeyUp
and KeyDown
events (as well as their tunneling counterparts) fire every time the user presses a key, but the PreviewTextInput
and TextInput
events fire only when a control receives actual input, which may be the result of multiple keystrokes. For example, pressing the Shift key to enter a capital letter would result in a KeyDown
event but no TextInput
event. When the user subsequently pressed the desired letter, a second KeyDown
event would fire, and finally the TextInput
event would fire.
Some controls that do advanced text handling, such as the System.Windows.Controls.TextBox
, suppress some of the keyboard events, meaning you may not always see the events when and where you would expect to handle them. If this is the case, you usually have to resort to using the Preview
events.
The following XAML demonstrates how to handle keyboard events. The example handles all keyboard events raised on a TextBox
control and logs them to another read-only TextBox
. Figure 12-8 shows the example running after the user has pressed Shift+L and then lowercase letter g. You can see that the TextInput
event is suppressed and so does not appear in the log.
<Window x:Class="Recipe_12_08.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 12_08" Height="300" Width="300"> <DockPanel LastChildFill="True"> <TextBox DockPanel.Dock="Top" FontSize="14" Height="30" HorizontalAlignment="Stretch" PreviewKeyDown="TextBox_KeyEvent"
KeyDown="TextBox_KeyEvent" PreviewKeyUp="TextBox_KeyEvent" KeyUp="TextBox_KeyEvent" TextInput="TextBox_TextEvent" PreviewTextInput="TextBox_TextEvent"/> <TextBox Name="txtLog" HorizontalAlignment="Stretch" IsReadOnly="True" VerticalScrollBarVisibility="Visible"/> </DockPanel> </Window>
The following code-behind contains the keyboard event handlers that write details of the events to the log:
using System; using System.Windows; using System.Windows.Input; namespace Recipe_12_08 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Handles all Key* events for the TextBox and logs them. private void TextBox_KeyEvent(object sender, KeyEventArgs e) { String msg = String.Format("{0} - {1} ", e.RoutedEvent.Name, e.Key); txtLog.Text += msg; txtLog.ScrollToEnd(); } // Handles all Text* events for the TextBox and logs them. private void TextBox_TextEvent(object sender, TextCompositionEventArgs e) { String msg = String.Format("{0} - {1} ", e.RoutedEvent.Name, e.Text);
txtLog.Text += msg; txtLog.ScrollToEnd(); } } }
You need to query the state of the keyboard to determine whether the user is pressing any special keys.
Use the IsKeyDown
and IsKeyToggled
methods of the static System.Windows.Input.Keyboard
class.
The static Keyboard
class contains two methods that allow you to determine whether a particular key is currently pressed or whether keys that have a toggled state (for example, Caps Lock) are currently on or off.
To determine whether a key is currently pressed, call the IsKeyDown
method, and pass a member of the System.Windows.Input.Keys
enumeration that represents the key you want to test. The method returns True
if the key is currently pressed. To test the state of toggled keys, call the IsKeyToggled
method, again passing a member of the Keys
enumeration to identify the key to test.
The following XAML defines a set of CheckBox
controls representing various special buttons on the keyboard. When the Button
is pressed, the program uses the Keyboard
class to test the state of each button and update the IsSelected
property of the appropriate CheckBox
(see Figure 12-9).
<Window x:Class="Recipe_12_09.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 12_09" Height="170" Width="200"> <StackPanel HorizontalAlignment="Center"> <UniformGrid Columns="2"> <UniformGrid.Resources> <Style TargetType="{x:Type CheckBox}"> <Setter Property="IsHitTestVisible" Value="False" /> <Setter Property="Margin" Value="5" /> </Style> </UniformGrid.Resources> <CheckBox Content="LeftShift" Name="chkLShift"/> <CheckBox Content="RightShift" Name="chkRShift"/> <CheckBox Content="LeftControl" Name="chkLControl"/> <CheckBox Content="RightControl" Name="chkRControl"/> <CheckBox Content="LeftAlt" Name="chkLAlt"/> <CheckBox Content="RightAlt" Name="chkRAlt"/> <CheckBox Content="CapsLock" Name="chkCaps"/> <CheckBox Content="NumLock" Name="chkNum"/> </UniformGrid> <Button Content="Check Keyboard" Margin="10" Click="Button_Click"/> </StackPanel> </Window>
The following code-behind contains the Button.Click
event that checks the keyboard and updates the CheckBox
controls:
using System.Windows; using System.Windows.Input; namespace Recipe_12_09 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window
{ public Window1() { InitializeComponent(); CheckKeyboardState(); } // Handles the Click event on the Button. private void Button_Click(object sender, RoutedEventArgs e) { CheckKeyboardState(); } // Checks the state of the keyboard and updates the checkboxes. private void CheckKeyboardState() { // Control keys. chkLControl.IsChecked = Keyboard.IsKeyDown(Key.LeftCtrl); chkRControl.IsChecked = Keyboard.IsKeyDown(Key.RightCtrl); // Shift keys. chkLShift.IsChecked = Keyboard.IsKeyDown(Key.LeftShift); chkRShift.IsChecked = Keyboard.IsKeyDown(Key.RightShift); // Alt keys. chkLAlt.IsChecked = Keyboard.IsKeyDown(Key.LeftAlt); chkRAlt.IsChecked = Keyboard.IsKeyDown(Key.RightAlt); // Num Lock and Caps Lock. chkCaps.IsChecked = Keyboard.IsKeyToggled(Key.CapsLock); chkNum.IsChecked = Keyboard.IsKeyToggled(Key.NumLock); } } }
Handle the tunneling counterpart of the event you want to suppress. In the event handler, set the Handled
property of the event argument object to the value True
.
Each of the main mouse and keyboard events like MouseDown, MouseUp, KeyDown
, and KeyUp
has tunneling event counterparts that start off at the top of the container hierarchy and travel down to the target control. These tunneling counterparts have the prefix Preview
on their name. By handling these preview events, you can intercept an event before it happens at the target control and suppress it.
Every preview event handler takes two arguments: a System.Object
that contains a reference to the event sender and an object that derives from System.Windows.RoutedEventArgs
that contains data specific to the event being handled. RoutedEventArgs
implements a Boolean
property named Handled
. By setting this property to the value True
in your event handler, you stop the subsequent bubbling event from firing, effectively suppressing the event.
The following XAML demonstrates how to suppress the Button.Click
event by handling the PreviewMouseDown
event in the container of the Button
:
<Window x:Class="Recipe_12_10.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 12_10" Height="100" Width="200"> <StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal" PreviewMouseDown="StackPanel_PreviewMouseDown"> <Button Content="Blocked" Click="Button_Click" Height="25" Margin="10" Width="70"/> </StackPanel> <Button Content="Not Blocked" Click="Button_Click" Height="25" Margin="10" Width="70"/> </StackPanel> </Window>
The following code-behind shows how to suppress the Button.Click
event by setting the Handled
property to True
in the PreviewMouseDown
event handler:
using System.Windows; using System.Windows.Input; namespace Recipe_12_10 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { MessageBox.Show("Button Clicked", "Button"); } private void StackPanel_PreviewMouseDown(object sender, MouseButtonEventArgs e) { e.Handled = true; } } }