Architect or Cobbler?
Good code starts with good design

Scribble is Done

Sunday, November 26, 2006

Or as done as it's going to get.  Adding the file functionality was one of the easiest bits to do as the StrokeCollection has a Save method which will serialize the Strokes to the ISF (Ink Serialized Format) format.  To recreate the StrokeCollection you simply use a constructor that takes a Stream.  I made a binary file that also included how many Scribble Canvases are open and saved to that.

So now that I'm done, how do I feel about WPF?

There are some pretty cool new features, and binding is excellent, but there are some glaring problems that really need to be fixed.

Lack of support for basic dialogs is not great, although you can use Window Forms dialogs there are some issues with interop, particularly when graphics are involved as there is vector/raster conversion needed.

VS2005 needs better design time support - I suppose this will come pretty quickly as Cider gets improved, and there's always Expression if you really want better design tools.  Or wait for VS Orcas :-)

The IntelliSense is patchy at best, every time you import a custom class the IntelliSense stops working (which isn't surprising as the schema is then broken).  Speaking to Adam at TechEd he was mentioning that there's a new metalanguage being created behind the scenes which means that IntelliSense won't have to use the schemas to generate context sensitive help.  I can't wait.

As usual you can download the code here.

For the next couple of weeks I'm going to busy with a graduate training program - so it may well be a bit quiet on here.  After that I'm into a round of new course development, so I'll be blogging about cool new stuff (or even old stuff) that interests me then.


# posted by James @ 1:23 PM   0 comments

Printing

Saturday, November 25, 2006

I decided to bite the bullet and just use the PrintPreviewDialog - so my app now has printing support.  You may recall from earlier that I used a Command attribute for the Print menus rather than a Click attribute.  There are several benefits to using the Command attribute:

  • In many of the WPF controls these commands are already coded fro you so you don't have to code it.
  • There is a standard mechanism for seeing if the command can be executed so the menu is automatically enabled.
  • The menus will automatically get Keyboard shortcuts added.
  • You can override the standard behaviour in a CommandBindings section

So as the Print and PrintPreview don't seem to be wired up, I need to wire up the commands using CommandBindings like this:

<Window.CommandBindings>
<CommandBinding Command="Print" Executed="OnPrint"
CanExecute="CanPrint"/>
<CommandBinding Command="PrintPreview" Executed="OnPrintPreview"
CanExecute="CanPrint"/>
</Window.CommandBindings>

Now I need to provide the Code for the CanPrint method, this method is a simple switch that indicates whether or not the menu can be enabled.  In my case, I've decided that if there are no active Scribble windows then you won't be able to print, and the code is self explanatory.

        void CanPrint(object sender, CanExecuteRoutedEventArgs  args)
{
args.CanExecute = (ActiveCanvas != null);
}

The Print functionality involves creating a PrintDialog, and allowing this to manage the printing, while the PrintPreview is a bit trickier.  I need to use a RenderTargetBitmap and a BmpBitmapEncoder to convert the Vector information into a raster bitmap that can be used in the Print Page handler for the PrintPreviewDialog.  There is one obvious disadvantage to this approach - the print quality will be lower going through the PrintPreview dialog compared to just printing directly.  The printing itself is just a matter of getting the Strokes collection from the active InkCanvas, and then invoking the Draw method for each Stroke in that collection.

        void OnPrint(object sender, RoutedEventArgs args)
{
PrintDialog pd = new PrintDialog();
if (pd.ShowDialog().GetValueOrDefault())
{
DrawingVisual dv = RenderSinglePage();
pd.PrintVisual(dv, "Scribble Canvas Print Job");

}
}

private DrawingVisual RenderSinglePage()
{
DrawingVisual dv = new DrawingVisual();
DrawingContext dc = dv.RenderOpen();
ScribbleCanvas sc = ActiveCanvas;
StrokeCollection strokes = sc.Strokes;
foreach (Stroke stroke in strokes)
{
stroke.Draw(dc);
}
dc.Close();
return dv;
}

void OnPrintPreview(object sender, RoutedEventArgs args)
{
PrintDocument pd = new PrintDocument();
pd.PrintPage += new System.Drawing.Printing.PrintPageEventHandler(
this.ScribblePrintPage);
System.Windows.Forms.PrintPreviewDialog prevDlg =
new System.Windows.Forms.PrintPreviewDialog();
prevDlg.Document = pd;
prevDlg.ShowDialog();

}

private void ScribblePrintPage(object sender, PrintPageEventArgs ev)
{

ScribbleCanvas sc = ActiveCanvas;
RenderTargetBitmap rtb = new RenderTargetBitmap((int)sc.ActualWidth,
(int)sc.ActualHeight,
96d, 96d,
PixelFormats.Default);
rtb.Render(sc);
MemoryStream ms = new MemoryStream();
BmpBitmapEncoder encoder = new BmpBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(rtb));
encoder.Save(ms);
ev.Graphics.DrawImage(new System.Drawing.Bitmap(ms),
0f,0f,
(float)sc.ActualWidth,(float)sc.ActualHeight);
ms.Close();
ev.HasMorePages = false;
}

As usual you can download the source code here


# posted by James @ 4:23 PM   0 comments

A little printing difficulty

Thursday, November 23, 2006

I was going to add the Print functionality to my Scribble app next, and the printing side was a piece of old rope - the printing support is excellent. Then I came to do the PrintPreview, and after 2 days I'm still a little stumped - although I have a working solution it feels Kludgy. There are 2 options that I can see (and which I have tested), both of them a little dodgy. The first is to take my InkCanvas and create an XPSDocument, and then encapsulate a DocumentViewer within a Dialog page, as DocumentViewers are WSYISWYG. The second is to use Windows Forms Interop, use the existing WinForms PrintPreviewDialog and use the RenderTargetbitmap class to turn the Vector graphic DrawingVisual into a raster Bitmap, which I can display in the WinForms dialog.

I almost can't believe this oversight, particularly as Microsoft have added PrintPreview as one of the standard ApplicationCommands and made it available via a property.

So I'm probably going to skip printing for a few days and concentrate on the File save/Open dialog.

If anyone can show me how to do a PrintPreview dialog in the meantime, then that would be cool


# posted by James @ 12:23 AM   0 comments

Style

Sunday, November 19, 2006

I've already blogged about style earlier in my blog, but styles are absolutely essential to the success of any Xaml app, particularly when you have many controls that share behaviour.  In the PenWidthDialog we've got two text boxes that we want to style the same.  I'd like their background to turn red, and a tooltip to appear with the error message returned from the ValidatorRule.  To do this you need to add a Resources section to the Xaml, give your style a key. like this:

 <Window.Resources>
<Style x:Key="MyTextBoxStyle" TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"
/>
<Setter Property="Background" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>

This probably needs a little bit of explaining.  The style has to know the types which it can be applied to , and the Triggers collection is a collection that contains Triggers.  Triggers are objects which are activated in response to either a property changing or an event being fired.  In our case the Trigger is fired when the property Validation.HasError changes to true.  At that time two properties of the TextBox are changed - the Tooltip property is set so that it displays the first error message in the Validation.Errors collection and the Background is set to red.  The RelativeSource property allows you to specify a source for the binding, instead of walking the control tree.


Now all you have to do is apply these styles to your textbox, and when you get a validation error, then it will look something like this:



We're nearly done.  All we have to do is sort out the printing, and saving and restoring the state.  I'm sure I'll get a chance next week to finish this off.


# posted by James @ 6:00 PM   0 comments

Adding some Dialogs to Scribble

Saturday, November 18, 2006

I'm going to add a dialog to the Scribble app so I can change the thicknesses of the pen lines.  The dialog will look like this

To do this you need a Window to represent the GUI, so I created a new Xaml file PenWidthDialog.xaml, gave it a border, a 4X4 grid, two text boxes and two buttons.  The only work at this stage is to set the IsDefault and IsCancel properties on the appropriate buttons.  I've also set the TabIndex and IsTabStop properties as you need to be able to tab through a dialog properly.

  <Border BorderThickness="2">

<Grid Background="LightGray">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" />
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="80" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label Grid.Column ="1" Grid.Row="1">Thin Pen Width</Label>
<Label Grid.Column ="1" Grid.Row="2">Thick Pen Width</Label>
<TextBox Grid.Column ="2" Grid.Row="1" Margin ="0,2,0,6"
IsTabStop="true" TabIndex="0">
</TextBox>
<TextBox Grid.Column ="2" Grid.Row="2" Margin ="0,2,0,6"
IsTabStop="true" TabIndex="1" >
</TextBox>
<Button Grid.Column ="3" Grid.Row="1" Height="25" Width="70"
Name="OKButton" Click="OnOkButtonClicked" IsTabStop="true"
TabIndex="2" IsDefault="True">OK</Button>
<Button Grid.Column ="3" Grid.Row="2" Height="25" Width="70"
Name="CancelButton" IsTabStop="true" TabIndex="3"
IsCancel="True">Cancel</Button>
</Grid>

</Border>

Now I want to add the data bindings for the Pen Width, and immediately I realise I need a bit of refactoring, so I create a Pen class with 3 properties, ThinWidth, ThickWidth and InkColor.  In my ScribbleCanvas class I create a new Pen and attach it to the canvas, and remove the old int properties that described thick and thin pens.


Now change Scribble.Xaml to add an event handler for the menu PenWidths, by adding a Click property.  The implementation of the event handler is a doddle, create a new PenWidth dialog object and show it using the ShowDialog method.  This returns a bool? , so it is important you handle the null value that is returned if the user closed the dialog with the X.  The simplest way to do this is to use the GetValueOrDefault method, which will convert a null to false.  If you clicked OK then simply call the ChangePenWidths() method to propagate the changes to all canvases.  Before you do this you have to set the static Pen property to the new Pen from the dialog.

        void OnChangePenWidths(object sender, RoutedEventArgs e){
PenWidthDialog pwd = new PenWidthDialog();
pwd.Owner = this;
if (pwd.ShowDialog().GetValueOrDefault())
{
ScribbleCanvas.ChangePen();
ChangePenWidths();
}
}

You can compile and run this, and though the dialog works, nothing will happen yet.  The reason is obvious, we haven't bound the text boxes to any data, so we'll fix that right now.  In WPF a control attempts to bind to a DataContext, and it will simply search up it's control tree till it finds a non-null DataContext and use this as the data source.  Then you use the Binding element to attach to this context. The binding has a Path property, which describes the property in the context that you wish to bind against. In our case this will be the ThinWidth and ThickWidth properties of the Pen object that belongs to the ScribbleCanvas.  To set up this data context I'm going to add an initialisation method that is invoked by adding the Loaded property to my Xaml, I'm also going to take the opportunity to add my WPFScribble namespace into the Xaml as I know I need it just now, as in:

<Window x:Class="WPFScribble.PenWidthDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:WPFScribble"
Title="Pen Widths" Height="150" Width="300" ResizeMode="NoResize"
Loaded="OnInit"
>

In the OnInit method I'm going to load the DataContext with the Pen object that I want to bind to.

        private void OnInit(object sender, RoutedEventArgs e)
{
this.DataContext = ScribbleCanvas.ScribblePen;
}

Finally you add the binding that will bind the properties to the text boxes.  To do this you  need to add either a Binding element to the Text property or  attach a {Binding} statement directly to the text box.   Both syntaxes are shown below.

      <TextBox Grid.Column ="2" Grid.Row="1" Margin ="0,2,0,6" 
IsTabStop="true" TabIndex="0">
<TextBox.Text>
<Binding Path="ThinWidth"
UpdateSourceTrigger="LostFocus" />
</TextBox.Text>
</TextBox>
<TextBox Grid.Column ="2" Grid.Row="2" Margin ="0,2,0,6"
IsTabStop="true" TabIndex="1" Text="{Binding Path=ThickWidth}" />

You can run your application now, and changing Pen widths will work, well to a point.  Open up the dialog and make the thin width 3 and the thick width a.  Clearly this is a nonsensical value, but the GUI is quite happy to accept this.  Oops, we need to add validation.  WPF only comes with one validator the ExceptionValidationRule, which fires if an exception is thrown when updating the binding.  This isn't good enough for our needs, so we need to create our own validator.  To do this you subclass ValidationRule, provide any properties you need, such as a minimum and maximum value, and override the Validate method.  This returns a ValidationResult, as shown in the code below.

    public class PenWidthRule : ValidationRule
{
private int _min;
private int _max;

public PenWidthRule()
{
}

public int Min
{
get { return _min; }
set { _min = value; }
}

public int Max
{
get { return _max; }
set { _max = value; }
}

public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
int age = 0;

try
{
if (((string)value).Length > 0)
age = Int32.Parse((String)value);
}
catch (Exception e)
{
return new ValidationResult(false, "Illegal characters");
}

if ((age < Min) || (age > Max))
{
return new ValidationResult(false,
"Please enter an age in the range: " + Min + " - " + Max + ".");
}
else
{
return new ValidationResult(true, null);
}
}
}

You want the GUI to invoke the validation, so you need to add the validation rules to the Binding, as shown in the Xaml fragment below.  If the validation fails, then the WPF framework will surround the text box with a red surround. 

         <Binding Path="ThinWidth" 
UpdateSourceTrigger="LostFocus" >
<Binding.ValidationRules>
<src:PenWidthRule Min="1" Max="30" />
</Binding.ValidationRules>
</Binding>

You may want to have a custom validation message, to do this you need to change the style of the text box.  We'll leave this till tomorrow :-)  For now you can download the source for the project from here.


# posted by James @ 3:43 PM   0 comments

Thick Pens .. or is that thick coders

Thursday, November 16, 2006

This morning on the train, I thought I'd add the Pen functionality to my application, and I immediately jumped in the deep end, but after a few minutes I started to get the feel that my code was getting distinctly whiffy.  I was adding all sorts of functionality to the Window that wasn't the window's responsibility, and I had to stop and refactor ... See this is a design blog after all :-)

I really needed to extract the Pen width and colour setting into it's own class.  So I decided to create a UserControl.  To do this is fairly straightforward, simply create a Xaml file that is either a UserControl or has a root element that is the control you want to subclass.  In my case I want an InkCanvas object, so I'm going to create the following Xaml

<InkCanvas x:Class="WPFScribble.ScribbleCanvas"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

</InkCanvas>

This is pretty straightforward, it simply hooks up a code-behind file ScribbleCanvas, which must be a subclass of InkCanvas. It looks something like this:

public partial class ScribbleCanvas : InkCanvas
{
public ScribbleCanvas()
{
InitializeComponent();
}
}

The last thing to do is to change the Scribble.xaml to include our user control.  Since this is an XML file you have to use namespaces to define ownership, so you begin by defining a new namespace, in my case I decided to call it scribble, as follows

    xmlns:scribble="clr-namespace:WPFScribble"

Now you can replace references to the InkCanvas with references to your UserControl ScribbleCanvas like this

       <TabItem Header="Window1">
<scribble:ScribbleCanvas>
<scribble:ScribbleCanvas.DefaultDrawingAttributes>
<DrawingAttributes
Width="2"
Height="2"/>
</scribble:ScribbleCanvas.DefaultDrawingAttributes>
</scribble:ScribbleCanvas>
</TabItem>

And if you run it there should be no difference in functionality.  Of course if I was doing the refactoring properly, there would be some tests to ensure the behaviour is identical, but I never claimed to be perfect.


So, a short little posting tonight, but I'm cream crackered.  Tomorrow I'll add the menus to do the pen width and colour settings.


# posted by James @ 9:05 PM   0 comments

MDI - or not

Wednesday, November 15, 2006

Today I thought I'd do the windows, and then I found out that WPF doesn't support MDI (actually I knew it didn't, I was hoping that somehow MDI had sneaked into RTM). I could of course just use Windows Forms Integration and simply drag a Windows Forms Window in and use that through Forms interop, but I really wanted a XAML only solution. The alternative is to create your own window manager and to do it that way, but that's just way too much work :-)

So I've decided to use the TabControl, after all it's topical seeing as IE7 is out with it's own tabs, so that's what we're going to do in this session - hook up a TabControl to the form and then make it respond to the Window Open and Window Close menu commands.

Firstly I've had to modify the menu, as Window tile and cascade make no sense with a tab option. So I've altered the XAML so that my Window menu is now:

<Menu Background="White">
<MenuItem Header="Window" >
<MenuItem Header="New Window" Click="NewTab"/>
<MenuItem Header ="Close Window" Click="CloseTab" />
</MenuItem>
</Menu>

Notice I've hooked the click events to the methods NewTab and CloseTab, we won't implement them yet, as I need to add the TabControl to my XAML at this point. I'm going to add it to the MainWindow DockPanel that was created earlier, and it looks something like this:

 <DockPanel Name="DockPanel_MainDisplay"
Background="DarkGray"
Grid.Row="2">
<TabControl Name="MainWindow_TabPanel" TabStripPlacement="Top">
<TabItem Header="Window1">
</TabItem>
</TabControl>
</DockPanel>

A TabControl is simply a collection of TabItems. Each TabItem has a header which is it's title, and some visual content - for now we'll simply leave the visual content out.

If you build and run the application, then you'll see something like this:



Now, to hook the events up, you need to implement the NewTab and CloseTab methods in the code behind file Scribble.xaml.cs. The logic is fairly straightforward, I create a new TabItem and then I add it to the TabControl's Items collection. The TabControl is visible to me because I gave it a Name attribute in the XAML. I then grab the focus, and that's it.

If you do significant actions with a RoutedEvent such as changing the layout or drawing to screen, you should indicate this by setting the Handled attribute to true. This prevents any other handler between you and the root node from dealing with the event.

The CloseTab functionality is equally straightforward, simply remove the selected TabItem from the Items collection (I've also made some logic to rename the tabs, it's probably not necessary, I just wanted to see if it worked)

     void NewTab(object sender, RoutedEventArgs e)
{
WindowCount++;
TabItem item = new TabItem();
item.Header="Window"+WindowCount.ToString();
MainWindow_TabPanel.Items.Add(item);
item.Focus();
e.Handled = true;
}


void CloseTab(object sender, RoutedEventArgs e)
{
MainWindow_TabPanel.Items.Remove(MainWindow_TabPanel.SelectedItem);
WindowCount--;
// because of my simple naming scheme I need to rename the tabs
RenameTabs();
e.Handled = true;
}

You can build and run , and you can now add and remove tabs to the application. Of course you could (if you weren't as lazy as me) add a context sensitive menu to each tab to add and close tabs.

So far this isn't very exciting, I still can't scribble on the application, but before I went messing around creating mouse handlers, I noticed there was this Canvas class called the InkCanvas, so I wondered what functionality it offered.

So I simply added it to the XAML hierarchy, and bingo I got full Scribble functionality. I was that excited I could have kissed someone from the WPF team (except Rob, I've met him and the moustache would tickle). To add the functionality involved 2 changes, one to the XAML and one to the code as outlined below:

 <TabControl Name="MainWindow_TabPanel" TabStripPlacement="Top">
<TabItem Header="Window1">
<InkCanvas />
</TabItem>
</TabControl>
...item.Content = new InkCanvas();

My GUI is very nearly complete, I need functionality for Pen Thicknesses, and I may even add Pen Colour as well, just to really push the boat out, but that will have to wait till tomorrow, for now here's my Scribble app doing its stuff.




You can download my VS2005 project here.


# posted by James @ 6:47 PM   2 comments

Still broken

Tuesday, November 14, 2006

I thought I'd found the problem, but not yet :-)

Update - I appear to have it fixed now, fingers crossed, my archive settings disappeared when moving from the original to the beta. A quick delete of all the archive files, reset of the settings and its back to normal.


# posted by James @ 5:01 PM   0 comments

Beta Blogger Broken?

That'll teach me to move to a beta product. My archives are temporarily stuffed, and the Publish feature is completely off the wall. Since I've made a template change I get extremely erratic publishing, and most of the time a rather cryptic error message: org.apache.commons.net.ftp.FTPConnectionClosedException: Connection closed without indication.

Any ideas?

# posted by James @ 4:39 PM   1 comments

WPF Scribble - Part 1 The GUI

Someone asked me why there wasn't a Scribble sample app for WPF.  Maybe there is, but I thought it would be a great opportunity for me to play more with the technology, so if time allows (and time is in very short supply at this time of year) I'm going to build a Scribble app over a few days.  I'll make the code available as I go through the exercise.

 

The first step is to make sure you can write WPF apps.  If you have Vista and VS2005 then you're laughing.  If you don't you need to head off to MS, download the .Net framework 3.0, the Windows SDK and the VS 2005 extensions for WPF and WCF.  Don't worry it's only a few hundred Megs, should only take a few minutes on that super connection of yours.

 

Now to create a WPF application is fairly straightforward > Simply create a new project based on the Windows Application (WPF) template that the extensions have added for you. 

 

This will create two XAML files , App.xaml and Window1.xaml along with their code behind files.  At this point I usually either delete Window1.xaml or rename it.   Rename Window1.xaml to Scribble.xaml and then open App.xaml.  You will notice it is simply a holder for resources, but for now all you have to do is change the StartupUri attribute to be Scribble.xaml.

For the rest of this session you'll be working in either Scribble.xaml or the code behind file Scribble.xaml.cs, and all we're going to do in this session is create the basic GUI. For those of you have never seen the Scribble application, it's a simple MDI (multiple Document Interface) app where you can scribble stuff and save and retrieve it.  It looks a bit like

The basic GUI is a window, so Scribble.xaml currently has a Window root entry.  It also has a Grid container, which is exactly as its name suggests a grid where you can place components.  Our grid is fairly straightforward, it has 4 rows (1 for the menu, 1 for the toolbar, 1 for the main window and 1 for the status bar) so to start you will define this grid like this:

<grid background="White" name="DocumentRoot">
  <grid.rowdefinitions>
    <rowdefinition height="Auto">
      <!-- Menu -->
    <rowdefinition height="Auto">
      <!-- Tool Bar -->
    <rowdefinition height="*">
      <!-- Content Area -->
    <rowdefinition height="Auto">
      <!-- Status Bar -->
  </grid.rowdefinitions>
</grid>

You can see this is fairly straightforward, the 3 rows with embedded controls are all set to automatic height adjustment, while the row preserved for the content is scaled to fill the rest of the window.

 

Before I show you the completed XAML for the GUI, just a brief mention about layout controls.  You won't add items into the grid directly, rather you will use layout controls like DockPanels, which dock to the grid element, StackPanels which stack on top of each other or Panels which you can place anywhere.  This gives the most flexibility to control the layout, so you are going to add 4 DockPanels to the grid, like this:

<!-- Menu Bar -->
<DockPanel
    Name="DockPanel_Menu"
    Grid.Row="0" />
<!-- Tool Bar -->
<DockPanel
   Name="DockPanel_Toolbar"
   Grid.Row="1">
<DockPanel Name="DockPanel_MainDisplay"
   Background="DarkGray"
   Grid.Row="2">
</DockPanel>
<DockPanel Name="DockPanel_StatusBar"
   Grid.Row="3">
</DockPanel>



Now all you have to do is add the menu, the toolbar and the status bar.  The menus are self explanatory, and the status bar is fairly straightforward too.  The toolbar no longer needs an ImageStrip, simply add an image to the button.  For now that image will be stored on the file system, but later in the project you'll turn them into resources.  The XAML now looks like:

 

    <DockPanel
Name="DockPanel_Menu"
Grid.Row="0">
<Menu Background="White" >
<MenuItem Header="File" Click="NotImplementedMsg">
<MenuItem Header="New" />
<MenuItem Header="Open..." />
<MenuItem Header="Close" "/>
<MenuItem Header="Save" />
<MenuItem Header="Save As..." />
<Separator />
<MenuItem Header="Print" />
<MenuItem Header="Print Preview" />
<MenuItem Header="Exit" InputGestureText="Alt-F4"
Click="ExitApplication">
<MenuItem.ToolTip>
<ToolTip>
Click here to exit
</ToolTip>
</MenuItem.ToolTip>
</MenuItem>
</MenuItem>
</Menu>
<Menu Background="White">
<MenuItem Header="Edit">
<MenuItem Header="Clear All" />
</MenuItem>
</Menu>
<Menu Background="White">
<MenuItem Header="Pen">
<MenuItem Header="Thick Pen" />
<MenuItem Header="Pen Widths..." />
</MenuItem>
</Menu>
<Menu Background="White">
<MenuItem Header="View">

<MenuItem Header="Toolbar" IsCheckable="True"
Checked="SetToolBarVisibility"
Unchecked="SetToolBarVisibility" IsChecked="True" />
<MenuItem Header="Status Bar" IsCheckable="True"
Checked="SetStatusBarVisibility"
Unchecked="SetStatusBarVisibility" IsChecked="True" />
</MenuItem>
</Menu>
<Menu Background="White">
<MenuItem Header="Window">
<MenuItem Header="New Window" />
<Separator />
<MenuItem Header="Tile" />
<MenuItem Header="Cascade" />
</MenuItem>
</Menu>
<Menu Background="White">
<MenuItem Header="Help">
<MenuItem Header="Help" />
<Separator />
<MenuItem Header="About WPFScribble" />
</MenuItem>
</Menu>
</DockPanel>

<!-- Tool Bar -->
<DockPanel
Name="DockPanel_Toolbar"
Grid.Row="1">
<ToolBar Name="toolBar" Button.Click=="NotImplementedMsg" >
<Button ToolTip="New">
<Image Source="Images\new.bmp" Height="16" Width="16" />
</Button>
<Button ToolTip="Open">
<Image Source="Images\open.bmp" Height="16" Width="16" />
</Button>
<Button ToolTip="Save">
<Image Source="Images\save.bmp" Height="16" Width="16" />
</Button>
<Button Command="ApplicationCommands.PrintPreview" ToolTip="Print Preview">
<Image Source="Images\preview.bmp" Height="16" Width="16" />
</Button>
<Button Command="ApplicationCommands.Print" ToolTip="Print">
<Image Source="Images\print.bmp" Height="16" Width="16" />
</Button>
<Button ToolTip="Help">
<Image Source="Images\help.bmp" Height="16" Width="16" />
</Button>

</ToolBar>
</DockPanel>
<DockPanel Name="DockPanel_MainDisplay"
Background="DarkGray"
Grid.Row="2">
</DockPanel>
<DockPanel Name="DockPanel_StatusBar"
Grid.Row="3">
<StatusBar Name="statusBar">
<StatusBarItem>
Press F1 for Help
</StatusBarItem>
</StatusBar>
</DockPanel>

 

The MenuItem click events are simply being routed to a NotImplementedMsg method in the code behind.  This is coded up as:

 

        void NotImplementedMsg(object sender, RoutedEventArgs e)
{
MessageBox.Show("This function is not yet implemented", "Patience");
}
 

This is a fairly standard event wiring up model, except for the EventArgs object which is a RoutedEventArgs object.  WPF routes events (So does Windows Forms, it just routes them differently) up a tree hierarchy, so if an event is not handled it bubbles up the tree until it finds a handler.  You can also use a tunneling strategy, where the event goes to the tree root first, and then via the children.  You can of course still use a direct strategy, analogous to Windows Forms where only the listener registered for events gets the chance to handle the event.  in the next installment you'll find out how to deal with RoutedEvents properly.


 


If you'd like the code for this then you can download a zip from here.


# posted by James @ 4:18 PM   2 comments

Styling WPF Buttons

Saturday, November 11, 2006

I was at TechEd last week and I was asked several times how to customize buttons. The answer is pretty straightforward, you use styles. If you're an ASP.Net developer that won't scare you, but if you're a Windows Forms developer then you may need a quick refresher.

You can apply a style to any control, and give default values for properties, so for instance you could create a style template that always coloured your buttons red, something like this:

<Window.Resources>
<Style x:Key="MyRedButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Background" Value="Red" />
</Style>
</Window.Resources>



To use this style in your button is fairly trivial, you simply reference the key you created as in the following code:


<Button Style="{StaticResource MyRedButtonStyle}"> Hello</Button>


Of course this simply allows you to change the basic properties, what if I want to change the entire look and feel of the Button control - well in that case you need to provide a control template. At first glance this appears more complex, but it isn't really that bad.


<Window.Resources>
<Style x:Key="MyButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<Ellipse Name="EllipseButton"

Width="{TemplateBinding Width}"

Height="{TemplateBinding Height}" >
<Ellipse.BitmapEffect>
<BevelBitmapEffect BevelWidth="3"

Relief=".6" >

</BevelBitmapEffect>
</Ellipse.BitmapEffect>

<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="Aqua" Offset="0" />
<GradientStop Color="Gray" Offset=".9" />
</LinearGradientBrush>
</Ellipse.Fill>

</Ellipse>
<ContentPresenter TextElement.FontSize="{TemplateBinding FontSize}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" />
</Grid>

</ControlTemplate>
</Setter.Value>
</Setter>
</Style>



When you provide a control template you need to do all the redrawing, as it currently stands your button UI doesn't change when the button is clicked, so you need to provide a trigger to update the template as the user clicks on the button, as shown below:


<Window.Resources>
<Style x:Key="MyButtonStyle" TargetType="{x:Type Button}">

. . .

</Grid>
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsPressed" Value="True" />
</MultiTrigger.Conditions>
<Setter TargetName="EllipseButton" Property="Ellipse.Fill" >
<Setter.Value>
<LinearGradientBrush StartPoint="0,1" EndPoint="0,0">
<GradientStop Color="Aqua" Offset="0" />
<GradientStop Color="Gray" Offset=".9" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter TargetName="EllipseButton" Property="Ellipse.BitmapEffect" >
<Setter.Value>
<BevelBitmapEffect BevelWidth="1" Relief=".1" ></BevelBitmapEffect>
</Setter.Value>
</Setter>

</MultiTrigger>
</ControlTemplate.Triggers>

</ControlTemplate>
</Setter.Value>
</Setter>
</Style>


You can customise any control in this way and the flexibility is incredible. Of course you could also do all of this through source code if you are completely set against XAML.


You should notice the TemplateBinding directives, these simply specify that the values in the XAML will be passed through to the template, as the example below shows, the height and width attributes are passed to the underlying template:


<Button Name ="myButton"
Style="{StaticResource MyButtonStyle}"
Height="80" Width="80" Click="OnClick">
Button</Button>


Running this gives you the following button , and clicking it changes the GUI, so all in all a great 10 minutes work.



# posted by James @ 5:13 PM   1 comments

Windows Live Writer

I'm trying out the new writer and it certainly beats Notepad :-)


# posted by James @ 4:13 PM   3 comments
This page is powered by Blogger. Isn't yours?