Introduction
After becoming a father, I found that it didn't take long for my daughter to want to do everything I did. And, since I spend a significant amount of time typing away at the keyboard, she started wanting to use the computer before she even turned one. So, I had a great idea, I'll just open up Notepad and let her type away. And that worked, for all of about 10 seconds, until she somehow ended up in the Control Panel with the Add Hardware wizard open. So, I searched the web for a baby proof application that she'd like. I found one on the Code Project (linked at the bottom). My daughter has used the program and loved it for quite some time. As I decided to start learning WPF, I needed a small project I could tackle to get started. And, rewriting the shape displaying application that my daughter liked so much seemed like a great candidate. So I did, and called it ShapeShow. I hope you're able to find this article useful for development purposes as well as for providing some fun for your kids.
Prerequisites
Running this application requires:
- Windows XP or Vista
- .NET Framework 3.5
- Visual Studio 2008 (to open the project)
The Shape Library
The first thing I wanted to do was define the possible shapes that would display on the screen. So, I created a shape library project that contained classes for each of the shapes to be displayed - circle, triangle, and square. Although, the circle and square classes also ended up representing ellipses and rectangles.
The goal of the shape library is to provide a random shape of a random color and size. In order to do this, I really didn't need to know anything about the actual shapes. I only need a class that could create these shapes. That sounded like a good opportunity to use the Factory pattern. So, I decided to create an interface for a shape and implement that interface for each of the shape types. Although, a base class may have worked better in this case, since each shape implements its members in almost the same way. Below is the method called from the WPF application to get new random shapes.
public static IShape CreateShape()
{
return CreateShape((ShapeType)RandomNumber.Next(0, ShapeCount));
}
It calls an overloaded method that takes a shape type. The shape type is an enumeration, and is randomly selected by casting a random integer to a shape type. This, in turn, calls another overloaded CreateShape
method that selects a random color for the shape. And finally, the shape is created in the method shown below:
public static IShape CreateShape(ShapeType shapeType, ShapeColor shapeColor)
{
IShape shape = CreateShapeFromType(shapeType);
shape.Height = RandomNumber.NextDouble() *
(SystemParameters.VirtualScreenHeight / 2) + MinHeight;
shape.Width = RandomNumber.NextDouble() *
(SystemParameters.VirtualScreenWidth / 2) + MinWidth;
shape.Top = RandomNumber.NextDouble() *
(SystemParameters.VirtualScreenHeight - shape.Height);
shape.Left = RandomNumber.NextDouble() *
(SystemParameters.VirtualScreenWidth - shape.Width);
shape.FillBrush = CreateBrushFromColor(shapeColor);
return shape;
}
The shape type and color are selected through a simple set of Case
statements which select the correct shape to create and the correct brush to fill with. The shape size is bound by the screen size, and has a minimum height and width as well.
The WPF Application
The WPF application doesn't have a lot of work to do. It only needs to request a new shape each time a key is pressed and display that shape on the screen. In order to do this, I simply override the OnKeyDown
method. But, I also wanted to do other things there, mainly, give the user a way to close the application through a special key combination. And, that's exactly what I've done, through an Options menu. Below is the code that overrides the OnKeyDown
method:
protected override void OnKeyDown(KeyEventArgs e)
{
if (IsRequestingOptions)
{
ShowOptions();
}
else if (CanDraw(e))
{
DrawShape(ShapeFactory.CreateShape());
}
base.OnKeyDown(e);
}
And, drawing the shape is as simple as adding the shape to a canvas element, which I've called the DrawingSurface
. I've also set a limit on the number of shapes that can be displayed on the screen at once. This helps to keep the screen from getting too cluttered, and keeps memory usage down as well.
private void DrawShape(IShape shape)
{
if (DrawingSurface.Children.Count == MaxShapeCount)
{
DrawingSurface.Children.RemoveAt(0);
}
DrawingSurface.Children.Add(shape.UIElement);
}
As I noted earlier, since the application is full screen, there are no close or minimize buttons. So, I've provided a special key combination that can open an Options screen. The Options screen displays three options: Clear Screen, Return to ShapeShow, and Exit ShapeShow. The user can bring up the options screen by pressing Ctrl+Alt+O. To show the options, we just set the options user control's Visibility
property to be Visible
. And, I also lower the opacity on the drawing surface to visually signal that the drawing surface is deactivated. The user control is a simple StackPanel
with three buttons orientated vertically. Below is the code and a picture of the options control:
<StackPanel x:Name="ButtonPanel">
<Button Name="ClearScreen" Content="Clear Screen" Height="60" Width="Auto"
Style="{DynamicResource OptionsButton}"></Button>
<Button Name="ReturnButton" Content="Return To ShapeShow" Height="60" Width="Auto"
Style="{DynamicResource OptionsButton}"></Button>
<Button Name="CloseButton" Content="Exit ShapeShow" Height="60" Width="Auto"
Style="{DynamicResource OptionsButton}"></Button>
</StackPanel>
As you can see, I also create my own buttons. This is amazingly simple in WPF. I simply declare a Button
as usual and apply a Style
. Style
s are extremely flexible and easy to use. I've put my button styles in a separate file called Resources.xaml. The style for these buttons, called OptionsButton
, is shown below:
<Style x:Key="OptionsButton" TargetType="Button">
<Setter Property="Margin" Value="10" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid Height="{TemplateBinding Height}" Width="{TemplateBinding Width}">
<Rectangle x:Name="ButtonRect" RadiusX="10" RadiusY="10"
StrokeThickness="2" Stroke="#555555"
Style="{StaticResource OptionsButtonUp}" />
<ContentPresenter x:Name="ButtonContent"
Content="{TemplateBinding Content}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextElement.FontSize="20"
TextElement.Foreground="#C8C8C8"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ButtonRect" Property="Style"
Value="{StaticResource OptionsButtonOver}" />
<Setter TargetName="ButtonContent" Property="TextElement.Foreground"
Value="#FFFFFF" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="ButtonRect" Property="Style"
Value="{StaticResource OptionsButtonDown}" />
<Setter TargetName="ButtonContent" Property="TextElement.Foreground"
Value="#AAAAAA" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
While this may look a little complicated at first, it's really not. First, a Style
element must be declared, with a Key
and TargetType
. In this case, the style is applied to buttons in the options menu, so the Key
is OptionsButton
and the TargetType
is Button
. The properties are then set by declaring a Setter
element that defines the property to set and the value to use. Setting the margin property is a simple example. But, notice the amazing amount of flexibility available in setting the button template. Each button can be composed of whatever UI elements you'd like. In this case, I've used a Rectangle
(enclosed by a Grid
) as my button content. I've also set the Rectangle
's Fill
property to be a static resource defined in the same file. This allows me to easily change the look of the button on the mouse over and mouse down events. These are controlled by the Trigger
elements shown above. Also note the TemplateBindings above. These allow properties to be set based on the actual button properties. For instance, in the example above, the Grid
takes on the height specified by the button element in our ButtonControl
class.
The final step is to add the user control to the main window and hide or show it when appropriate. The XAML for the main window is shown below:
<Window x:Class="ShapeShow.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:uc="clr-namespace:ShapeShow"
Title="ShapeShow" ResizeMode="NoResize"
WindowStyle="None" WindowState="Maximized" Topmost="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Foreground="Gray" Grid.Row="0"
Padding="10,0">Press Ctrl+Alt+O to show options.</TextBlock>
<Canvas x:Name="DrawingSurface" Grid.Row="1" />
<uc:OptionsControl x:Name="Options"
Grid.RowSpan="2" Visibility="Collapsed" />
</Grid>
</Window>
When adding a user control, you have to be sure to add an XML namespace for that control, even if the control is in the same assembly, like this one. Note the xmlns:uc="clr-namespace:ShapeShow"
line that does this. And, ensuring that the window displays in full screen and stays on top is fairly simple, you just set the WindowStyle
to None
, the WindowState
to Maximized
, and TopMost
to true
. Also note the Height="*" for the second row definition. This tells the second row to take up all remaining space. And that is where the drawing surface resides. Finally, we're able to get the options menu to display over both rows by simply giving it a RowSpan of 2.
The Keyboard Hook
Low-level keyboard hooks aren't the focus of this article. It is much better explained in many other places. If fact, Emma's article (linked at the bottom) covers it well. However, I'll go ahead and provide a bit of general information on how it works.
Even though I have overridden the OnKeyDown
method, there are still some key combinations that I don't have control over. These are system keys such as the Windows keys or Alt+Tab. In order to stop these keys from being processed, I had to catch them with a system wide low-level keyboard hook. This is actually fairly easy to do. All that's needed is a function to handle the key press events and a few calls to user32.dll functions to set the hook. These functions are shown below:
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook,
HookHandlerDelegate callbackPtr,
IntPtr hInstance, uint dwThreadId);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
IntPtr wParam, ref KBHookStruct lParam);
Now, we only need to define a callback function for the hook and pass along the keys that we want to allow. We pass along keys by using the CallNextHookEx
function. And, we can discard keys by simply returning from the function without the call to CallNextHookEx
. The callback function must have a specific signature as shown below:
private static IntPtr KeyboardHookHandler(int nCode, IntPtr wParam,
ref KBHookStruct lParam)
{
if (nCode == 0)
{
if (((lParam.vkCode == 0x09) && (lParam.flags == 0x20)) ||
((lParam.vkCode == 0x1B) && (lParam.flags == 0x20)) ||
((lParam.vkCode == 0x1B) && (lParam.flags == 0x00)) ||
((lParam.vkCode == 0x5B) && (lParam.flags == 0x01)) ||
((lParam.vkCode == 0x5C) && (lParam.flags == 0x01)) ||
((lParam.vkCode == 0x73) && (lParam.flags == 0x20)) ||
((lParam.vkCode == 0x20) && (lParam.flags == 0x20)))
{
return new IntPtr(1);
}
}
return CallNextHookEx(hookPtr, nCode, wParam, ref lParam);
}
And That's It
We've got a complete full screen WPF application. Feel free to ask questions or make suggestions. I'd like to extend this in the future to also display other shapes, such as numbers or letters. So, if you have any other interesting ideas, please let me know. Thanks for viewing my first CodeProject article, and I hope you've found something useful.
Here's a link to the article that inspired this one, by Emma Burrows - Low-level Windows API hooks from C# to stop unwanted keystrokes.