WPF: Dive into WPF

tagged: code .net wpf

imageFor this first post on WPF I've decided it would be best to start off with a "real" application.  This means we'll need those thing almost all applications have; menus, status bars, and keyboard short-cuts.  I won't stop and make things pretty however, so the code stays clean, and I won't be using XAML.  Don't get me wrong, I think XAML is great, but it's not the first place to start with WPF.  The app I've put together is a simple image viewer that loads a list of images on the Internet and lets the user choose one to view.

To get started with this project, you'll need the WPF add-in for VS 2005 or VS 2008.  You'll also need .Net 3.0 installed if you are running XP.  Begin by creating a new C# Windows Application (WPF) project, then deleting the .xaml and .cs files created - we won't be needing them.  Create a new class, and call it MainWindow.  Here is the first part of the code for MainWindow:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using System.Windows.Media.Effects;

namespace FotoViewer {
    class MainWindow : Window {

        [STAThread]
        public static void Main() {
            Application app = new Application();
            app.Run(new MainWindow());
        }

        StatusBarItem sbiMessage;
        Image imgDisplayed;

        public MainWindow() {
            Title = "FotoViewer";

            InputGestureCollection gesExit = new InputGestureCollection();
            gesExit.Add(new KeyGesture(Key.X, ModifierKeys.Control));
            RoutedUICommand comExit = new RoutedUICommand("E_xit", "Exit", GetType(), gesExit);
            CommandBindings.Add(new CommandBinding(comExit, CloseApp));

            Menu menu = new Menu();
            MenuItem fileMenu = new MenuItem();
            fileMenu.Header = "_File";
            MenuItem fileExitItem = new MenuItem();
            fileExitItem.Command = comExit;
            fileMenu.Items.Add(fileExitItem);
            menu.Items.Add(fileMenu);

            StatusBar statusBar = new StatusBar();
            sbiMessage = new StatusBarItem();
            sbiMessage.Content = "FotoViewer Ready";
            sbiMessage.HorizontalAlignment = HorizontalAlignment.Left;
            statusBar.Items.Add(sbiMessage);

            Grid gBody = new Grid();
            gBody.ColumnDefinitions.Add(new ColumnDefinition());
            gBody.ColumnDefinitions.Add(new ColumnDefinition());
            gBody.ColumnDefinitions[0].Width = GridLength.Auto;
            gBody.ColumnDefinitions[1].Width = new GridLength(100, GridUnitType.Star);

            TreeView tvPhotos = new TreeView();
            tvPhotos.Padding = new Thickness(0, 0, 5, 0);
            tvPhotos.SelectedItemChanged += tvPhotos_SelectedItemChanged;
            FillTree(tvPhotos);

            imgDisplayed = new Image();
            imgDisplayed.HorizontalAlignment = HorizontalAlignment.Center;
            imgDisplayed.VerticalAlignment = VerticalAlignment.Center;
            imgDisplayed.Margin = new Thickness(15);
            imgDisplayed.BitmapEffect = new DropShadowBitmapEffect();

            Grid.SetColumn(imgDisplayed, 1);
            gBody.Children.Add(tvPhotos);
            gBody.Children.Add(imgDisplayed);

            DockPanel dpMain = new DockPanel();
            DockPanel.SetDock(menu, Dock.Top);
            DockPanel.SetDock(statusBar, Dock.Bottom);
            dpMain.Children.Add(menu);
            dpMain.Children.Add(statusBar);
            dpMain.Children.Add(gBody);
            Content = dpMain;

            tvPhotos.Focus();
        }

Don't worry too much over the namespaces for now - just know that WPF lives in the System.Windows namespace.  MainWindow derives from System.Windows.Window and that's what makes this able to use the WPF goodness.

The Main() method is nothing new to programmers - in fact it feels like an old friend returning from a long journey.  We can get away with having Main be part of a class it creates an instance of because the method is static; the STAThread is a hold over from Windows Programming of Yore (it's declaring a threading model) - needed still, yet matters little (we are still free to thread).  The app.Run handles the loading of the MainWindow and gets us ready for user input.

In the constructor (the remainder of the code thus far), we create all the elements of the window.  Title sets the Title bar text, and then we create a "Gesture" for Crtl+X and to that we attach a RoutedUICommand.  This allows the user multiple way to call the same action - in our case, exit the application.  The first way is given though the CommandBindings of MainWindow - any time the user hits Crtl+X, CloseApp() will be called.

Next is the Menu.  Menus hold collections of MenuItems, which in turn can have MenuItems of their own.  Our Menu just has a "File" menu, with one choice "Exit".  The underscores in the name allow the user to navigate the Menu with the Alt key.  Alt+F will open the file menu, then Alt+X will select Exit.  Notice we don't need to set the Header of fileExitItem - it is assigned when we set Command to comExit.  Infact, the item will read "Exit   Crtl + X" to let the user know about the shortcut tied to the same command.

The StatusBar is like the Menu in that it supports a collection of StatusBarItems (you do not nest StatusBarItems however).  The item we create is a private member of MainWindow - this will make it easier to access later from Event Handlers.

A Grid is just what it sounds like, a grid of rows and columns.  Here we create a two column grid - the first has a Width of GridLength.Auto.  This will size the the column based on the content.  The next column is set to a GridLength of GridUnitType.Star, causing it to take up the remaining space.  In this case the value of 100 is ignored.  If we had two columns of GridUnitType.Star, the value would be used to determine what percentage of length each column ended up being.  It is helpful to have all columns add up to 100 (i.e. 70-30 or 50-50) so you can think in percentage terms, but it's not required (4-3 works just as well).

The TreeView control is used for our navigation among images.  A Padding is set so the grid border is nicely spaced from the control, and an event handler is attached to the SelectedItemChanged event.  Notice that it's assigned with a += instead of a normal = sign.  This is because there can be more than one handler for an event and we don't want to break that assignment.  The FillTree method we'll cover in a moment.

Now we come to our other private member: imgDisplayed.  Nothing special here, but it's worth note that to add the drop shadow effect is one line of code applied to a Image control and not altering image data in memory (not by us anyway).

Placing our TreeView and Image controls in the Grid shows one of the interesting concepts in WPF - Attached Properties.  By default, items are added to the Grid at position Row 0, Col 0 - we need the Image at 0,1.  Instead of telling the Grid to place the Image at 0, 1 we "attach" a Column property with a value set to 1 on the Image control.

The Attached Property feature is also used in the DockPanel control.  DockPanel docks it's children to an edge of the window.  This makes it very easy to set the Menu at the top, StatusBar at the bottom and the Grid in the middle (by default, the last item added to a DockPanel fills the remaining space).

Last, the MainWindow Content property is set to the DockPanel control, so the window knows where to begin rendering the control tree.  We top it off by focusing input on the TreeView so the user can begin using the keyboard up and down arrow keys to move around the links.  Now let's look at the CloseApp() method:

        void CloseApp(Object sender, ExecutedRoutedEventArgs args) {
            if (MessageBoxResult.Yes ==
                MessageBox.Show("Really Exit?",
                                Title,
                                MessageBoxButton.YesNo,
                                MessageBoxImage.Question)
               ) Close();
        }

Nothing much here - a MessageBox uses one of the default styles to confirm the user really wants to exit, and if so calls Window.Close().  Now let's see how the tree is populated:

        private void FillTree(TreeView tvPhotos) {
            Dictionary<string, string[]> sites = new Dictionary<string, string[]>();
            sites.Add("I Can Has Cheezeburger?", new string[] { 
                "http://icanhascheezburger.files.wordpress.com/2007/07/notwantdecaf.jpg",
                "http://icanhascheezburger.files.wordpress.com/2007/07/surrender.jpg",
                "http://icanhascheezburger.files.wordpress.com/2007/07/no-payn.jpg",
                "http://icanhascheezburger.files.wordpress.com/2007/06/lolcat_this_is_mah_job.jpg"
            });
            sites.Add("ViNull Photos", new string[] {
                "http://farm1.static.flickr.com/98/233396598_3d60aae5e7_b.jpg",
                "http://farm1.static.flickr.com/93/233443569_6c1ea21bb2_b.jpg",
                "http://farm1.static.flickr.com/96/232827160_0b2d295f9a_b.jpg"
            });

            foreach (string site in sites.Keys) {
                TreeViewItem tviSite = new TreeViewItem();
                tviSite.Header = site;
                tviSite.IsExpanded = true;
                tvPhotos.Items.Add(tviSite);

                foreach (string link in sites[site]) {
                    Uri uri = new Uri(link);
                    TreeViewItem tviImage = new TreeViewItem();
                    tviImage.Tag = link;
                    tviImage.Header = uri.Segments[uri.Segments.Length - 1];
                    tviSite.Items.Add(tviImage);
                }
            }
        }

A hard-coded set of links is looped over creating TreeViewItems and attaching them to the TreeView.  The TreeViewItem.Tag property is an object you can use to save any information you want attached to that node on the tree.  You do not have to use TreeViewItem - the Items.Add method will take any object and add it to the tree, but in the common cases TreeViewItem is perfect for the job.  Last, the selected event:

        void tvPhotos_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) {
            TreeViewItem item = e.NewValue as TreeViewItem;
            if (item.Tag != null) {
                Uri uri = new Uri(item.Tag.ToString());
                BitmapImage bitmap = new BitmapImage(uri);
                imgDisplayed.Source = bitmap;
                sbiMessage.Content = item.Tag.ToString();
            }
        }
}
}

Because the TreeView isn't limited to just TreeViewItems, a generic form of arguments is used - I ignore this fact and just assume a TreeViewItem has been clicked.  We read the Tag property and then download the Image, convert it to a bitmapped format, set it to display, and update the statusbar.  Yes, all that happened in that short amount of code.

You may be wondering where the rest of the code is - there is no "OnRender" or "OnPaint" to updated the controls as I change them.  In WPF you normally don't need to get involved at that level due to Dependant Properties.  Simple put, Dependant Properties are properties flagged by WPF to watch for changes.  When a change is made, the controls are informed and given the chance to update themselves on screen if needed.  WM_PAINT may never be heard from again.

If you went though the effort to run this program you may notice a big problem - it's slow, or at least appears to run slow.  Why is this?  Well, we are downloading the image on the UI thread and not giving the user an update to know that it's happening.  Since we are hogging the thread, the user can't even hit Crtl+X to close it - we appear "locked up".  In my next post, we'll look at created a separate thread to handle downloading the image, and telling the user we are working in the background, so please don't kill our process with task manager.

0 Comments

Leave a comment



Your name:
 

Your email (not shown):
 

Your website (optional):