### wx.NET ImageViewer Tutorial

In this tutorial, we will create a simple image viewer using wx.NET. You can download the source and this tutorial, here. Premake build files are also included, to help you get everything going more quickly.

The viewer isn't designed to be state-of-the-art, and could be improved enormously. However, the point of this tutorial is to give an introduction to several wxWidgets concepts. After reading, you should know enough to get started in creating your own applications.

### Prerequisites

To get started, you should have a working wx.NET and .NET installation. See the downloads and building pages for more details.

NOTE: At the time of writing (wx.NET version 0.2), a CVS version of wx.NET is required. See the latest sources page to download an up-to-date copy.

Throughout the tutorial, I assume that you have a basic understanding of GUI programming, as well as the basic elements of a GUI (e.g. frames, menus, status bars, etc.) I also assume that you are somewhat familiar with C# programming. No advanced C# features are used, and only a very little of the .NET Framework is accessed.

### Getting Started - Creating the Application

At the heart of every wxWidgets application, there is a wx.App instance.

In our application, we create a class named ImageViewApp, derived from wx.App. We then override the OnInit method.

> /**
> * This is the application.
> */
> public class ImageViewApp : App
> {
> /**
> * The overrided OnInit is where the main form should be created and
> * shown. Application start-up logic can also be placed here.
> */
> public override bool OnInit()
> {
> // Return true, to indicate that everything was initialized
> // properly.
> return true;
> }

Then to start the application, we create an instance of the application object, and call its Run() method.

> [STAThread]
> public static void Main()
> {
> // Create an instance of our application class
> ImageViewApp app = new ImageViewApp();
> // Run the application
> app.Run();
> }
> }

At this point, the application will just sit there - nothing interesting will happen. So we will now create a wx.Frame class, and get down to business.

### Frame, Menus, and a Status Bar

For our main frame, we create the ImageViewFrame class, derived from wx.Frame. In the constructor of this class, we initialize the base class with the title of the frame, as well as a default position, and an initial size.

> /**
> * This is the main frame. All user interaction will start here.
> */
> public class ImageViewFrame : Frame
> {
> /**
> * The base class is passed the title of the frame, the default
> * position for its position, and an arbitrary size.
> *
> * All the components inside the frame are created and initialized
> * here.
> */
> public ImageViewFrame()
> : base("ImageView", wxDefaultPosition, new Size(500, 500))
> {

To add the newly created frame class, we add the following to our application's OnInit method:

> // Create the main frame
> ImageViewFrame frame = new ImageViewFrame();
> // Show it
> frame.Show(true);

After an instance of the frame is created, and Show() is called, the empty frame will be displayed. It's now time to add a few menus.

In the frame class, we also define several integer ID's which will be used when handling events generated by the menus, which we will create next.

> // Every control that we want to handle events for will need an
> // integer ID, these IDs are listed below.
> // File menu IDs
> private const int ID_FileOpenDir = 0;
> private const int ID_FileExit = 1;
> // Help menu IDs
> private const int ID_HelpAbout = 2;

We then begin by creating a wx.MenuBar in the frame's constructor. The menu bar class is portion of the menu where all sub-menus are attached.

> // The menu bar is the bar where all the menus will be attached
> // to.
> MenuBar menuBar = new MenuBar();

The first menu that we create will be the File menu. The ID's we declared here are passed to the Append() method, so that wxWidgets will be able to associate actions on the items with the event handlers that we declare.

> // The File menu
> Menu fileMenu = new Menu();
> // The first item we append is the "Open" command. The ID for
> // the menu item is passed, as well as two strings.
> //
> // The first string is the menu item text. The stuff after '\t'
> // tells wxWidgets to use this as a short-cut key for the item.
> // An '&' will underline the next character on the menu, for
> // easy access.
> //
> // The second string is the help text for the menu item. When a
> // users mouse hovers over the item, the status bar text will
> // change to this.
> fileMenu.Append(ID_FileOpenDir, "&Open Directory...\tCtrl+O",
> "Open a directory with images");
> // Append a menu seperator.
> fileMenu.AppendSeparator();
> // Exit menu item.
> fileMenu.Append(ID_FileExit, "E&xit\tCtrl+Shift+W",
> "Exit this fine application");
> // Attach the file menu to the menu bar
> menuBar.Append(fileMenu, "&File");

The next menu is the Help menu, with one item.

> // The Help menu
> Menu helpMenu = new Menu();
> helpMenu.Append(ID_HelpAbout, "&About...",
> "About this application");
> menuBar.Append(helpMenu, "&Help");

To get our frame to use the menu bar that we've created, we set the MenuBar property.

> // Next we tell the frame to use the menu bar that we've created,
> // using the Frame.MenuBar property.
> this.MenuBar = menuBar;

Our menu is now complete. It's time for a status bar. Our status bar will have two fields (even though we're only using one.) The status bar's text is then set with some welcome text using the StatusText property.

When using the StatusText property, the first field in the status bar will be set.

> // Create a status bar with 2 fields
> CreateStatusBar(2);
> // Set the initial status bar text, this will set the text in the
> // first status bar field.
> StatusText = "Welcome to ImageView!";

Our frame now has a menu bar, and a status bar, but it still doesn't do anything useful. Next we'll set up some event handlers, and do a little 'real' work.

### Handling Events

NOTE: The current version of wx.NET uses a macro-like system, similar to that used in wxWidgets C++, for handling events. Support for handling events using the prefered C# delegates is on our TODO list.

Attaching an event handler to an event, such as a mouse click, is a fairly straight forward process.

We've already created several menu items, and each has its own unique integer ID. We call the appropriate EVT_* method to attach the event to a method.

> // Set some event handlers using the IDs of the controls we
> // wish to handle events for.
> EVT_MENU(ID_FileExit, new EventListener(OnFileExit));
> EVT_MENU(ID_FileOpenDir, new EventListener(OnFileOpenDir));
> EVT_MENU(ID_HelpAbout, new EventListener(OnHelpAbout));
> // Handle when the frame is closed
> EVT_CLOSE(new EventListener(OnClose));
> }

The EVT_MENU calls are for handling menu events. The last call, with EVT_CLOSE, is an event for handling when the frame is closed. When an event occurs, wxWidgets will look-up the appropriate handler in the Frame instance, and call it.

We now define the event handlers.

Our first handler is for the Exit menu option. Here we just call Close() on the frame. This will trigger an event that will call our OnClose() handler. The important parameter for the event handler is the Event instance. With the Event class, we can find out more about the nature of the event, as well as who created and sent the event to us. We will use this later on in the application.

> /**
> * Exit event handler
> */
> protected void OnFileExit(object sender, Event evt)
> {
> // Close the frame. Since this is the last (only) frame in the
> // application, the application will exit when it is closed.
> Close();
> }

We save the logic for the OnFileOpenDir() handler until later. For now it will just sit empty.

> /**
> * Open Directory event handler
> */
> protected void OnFileOpenDir(object sender, Event evt)
> {
> }

In the Help event handler, all we want to do is display a simple message box with some information about the application. The static method, MessageBox.ShowModal() is used for this.

> /**
> * About event handler
> */
> protected void OnHelpAbout(object sender, Event evt)
> {
> // Message for our about dialog.
> string msg = "ImageView!\n\nAn application for viewing images.";
> // Display a message box. The message box will have an OK button
> // (wxOK), and an information icon (wxICON_INFORMATION)
> MessageDialog.ShowModal(this, msg, "About ImageView",
> Dialog.wxOK | Dialog.wxICON_INFORMATION);
> }

In the OnClose() handler, we can prevent the user from closing the application by not calling Event.Skip(). How does this work? When Event.Skip() is called, the event is treated as 'unhandled', and the base wxWidgets event handler is called. In this case the base handler will close the frame for good.

In the case that Event.Skip() is not called, the event is treated as 'handled'; the wxWidgets handler will not be called, and the frame won't be closed.

In this method, we use another message box. This time we capture the return value from ShowModal(), which will indicate what option the user has selected, so that we can act accordingly.

> protected void OnClose(object sender, Event evt)
> {
> // We can ask the user whether or not it is OK to close the frame,
> // then act appropriately.
> string msg = "Are you sure you'd like to exit?";
> int result = MessageDialog.ShowModal(this, msg, "Exit ImageView",
> Dialog.wxYES_NO |
> Dialog.wxICON_QUESTION);
> // Did the user click yes?
> if (result == Dialog.wxYES) {
> // They did, we tell wxWidgets to take care of closing the
> // application
> evt.Skip();
> }
> }

Our frame's menus now handle some events, and perform a couple of useful actions. Next we'll move to another portion of the application before enhancing the frame more.

### Custom Controls

We now look into the mechanism with which we will list the images in a directory. Each image will be on its own 'tile', consisting of a button with an image on it (BitmapButton), as well as a text label (StaticText).

This control will be in a class called Thumbnail, and will inherit from wx.Panel. The Panel class is ideal for laying out forms and creating new types of controls.

Thumbnail will have several members to store information about the image that it's displaying.

> /**
> * Class to display a thumbnail image.
> */
> public class Thumbnail : Panel
> {
> // Delegate to handle thumnail events
> public delegate void ThumbnailClick(string file, wx.Bitmap bmp);
> // Delegate for external classes to hook into click events
> public ThumbnailClick ThumbnailClicked;
> // The bitmap on this thumbnail
> private wx.Bitmap m_bitmap;
> // Filename of the bitmap to use
> private string m_fileName;
> // A bitmap button used to display the image
> private BitmapButton m_bitmapBtn;
> // A text label for the file name
> private StaticText m_label;

The constructor will use these elements to construct a bitmap and display the bitmap with the file name as a label.

> public Thumbnail(Window parent, string fileName)
> : base(parent)
> {
> m_fileName = fileName;

When developing cross-platform applications, it can be tedious to position each control in such a way that the GUI will look 'natural' on each platform. wxWidgets provides an easy to use solution for this: sizers.

Sizers are used to arrange controls in a variety of formats. The BoxSizer, for example, arranges controls in a line. A GridSizer arranges controls in a grid. There is also a FlexGridSizer, StaticBoxSizer, and a NotebookSizer.

Our thumbnail control will use a BoxSizer to arrange the image and text label in a vertical line.

> // A box sizer to arrange the controls vertically
> BoxSizer sizer = new BoxSizer(Orientation.wxVERTICAL);

We now load the image file that was passed through the constructor. The image is loaded using the wx.Image class, then stored in a wx.Bitmap for later use.

wx.Image can be used to easily manipulate images, and is used here to resize the image for the thumbnail.

> // The bitmap constructor can load the image files directly
> wx.Image image = new wx.Image(m_fileName);
> m_bitmap = new wx.Bitmap(image);
> wx.Bitmap smallBmp = m_bitmap;
> // Scale the bitmap to icon size, retaining the aspec ratio
> int max = Math.Max(m_bitmap.Width, m_bitmap.Height);
> if (max > 100) {
> double scale = 100.0 / (double)max;
> int width = (int)((double)m_bitmap.Width * scale),
> height = (int)((double)m_bitmap.Height * scale);
> wx.Image img = m_bitmap.ConvertToImage();
> img.Rescale(width, height);
> smallBmp = new wx.Bitmap(img);
> }

The image is now ready for use, and the BitmapButton is created, then added to our sizer.

When a control is added to a sizer, the sizer will take control of positioning and laying out the control.

The first parameter of Sizer.Add(), is our control. Then second is the proportion of this control with respect to the other controls that will be added to the sizer. The third parameter is a set of flags. Alignment.wxALIGN_CENTRE will ensure that the control is centered. Direction.wxALL will give the control a border around all edges. The fourth parameter defines how large that border will be, in this case, 3.

> // Create the bitmap button with the small bitmap
> m_bitmapBtn = new BitmapButton(this, -1, smallBmp);
> // Add it to the sizer, centered, with a small border
> sizer.Add(m_bitmapBtn, 1, Alignment.wxALIGN_CENTRE |
> Direction.wxALL, 3);
> // Create our static text label with the file name as the label
> m_label = new StaticText(this, -1, "");
> string lbl = m_fileName.Substring(m_fileName.LastIndexOf('/') + 1);
> lbl = lbl.Substring(0, lbl.LastIndexOf('.'));
> m_label.Label = lbl;

Next, we create the label that will be displaying the file name. It is also added to the sizer. Since our sizer has a vertical orientation, it will be below (or after) the BitmapButton.

> // Add the label to the sizer, centered, with a small border
> sizer.Add(m_label, 0, Alignment.wxALIGN_CENTRE |
> Direction.wxALL, 3);

The sizer is now assigned to the Panel. Sizer.SetSizeHints() is used to tell the sizer to ensure that the minimum size of the Thumbnail panel is large enough to fit its contents.

> // Set the sizer
> Sizer = sizer;
> sizer.SetSizeHints(this);

The Thumbnail control has all its GUI elements defined. So we'll do a little event handling for the BitmapButton. The event handler is tied in using a call to EVT_BUTTON, which operates like EVT_MENU.

> // Catch the bitmap button click event, a '-1' is used, since our
> // panel control has only one bitmap button, so the event can only
> // come from one place.
> EVT_BUTTON(-1, new EventListener(OnBitmapButtonClicked));
> }

Our Thumbnail class will use a delegate to transfer events to anything that's listening.

> protected void OnBitmapButtonClicked(object sender, Event evt)
> {
> // Notify our listeners
> if (ThumbnailClicked != null) {
> ThumbnailClicked(m_fileName, m_bitmap);
> }
> }

That's it. The Thumbnail class is ready to start displaying images, and passing events on to listeners. We'll now need a mechanism for displaying the bitmaps in their full form.

### Scrolling a Window

To display large images, we will need a window that can be used to scroll its contents. wxWidgets provides the wx.ScrolledWindow class to do this.

Here is the start of our ImageViewer class.

> /**
> * A class to view an image.
> *
> * A scrolled window is used so that large images can be viewed.
> */
> public class ImageViewer : ScrolledWindow
> {
> private wx.Bitmap m_bitmap, m_defaultBmp;
> public ImageViewer(Window parent)
> : base(parent)
> {
> // Load a default image (logo)
> m_defaultBmp = new wx.Bitmap("ImageView.bmp",
> BitmapType.wxBITMAP_TYPE_BMP);
> // Our panel will have a white background, the colour contructor
> // accepts strings for several common colours.
> BackgroundColour = new Colour("White");
> // Set our bitmap
> Bitmap = m_defaultBmp;
> }

The ImageViewer class will be reusable, so it has a bitmap property.

Every time a new bitmap is set, the scrollbars are also set with the bitmap's width and height. No furthur work is required - that is all we have to do to ensure that the window's contents are scrollable.

> /**
> * The bitmap to be displayed.
> */
> public wx.Bitmap Bitmap
> {
> set {
> if (value == null) {
> // Use the default
> m_bitmap = m_defaultBmp;
> }
> else {
> m_bitmap = value;
> }
> // Initialize the scrollbars
> SetScrollbars(1, 1, m_bitmap.Width, m_bitmap.Height,
> 0, 0, true);
> // Redraw the window
> Refresh();
> }
> }

To display the image, we will use a Device Context (DC). A DC is used to access a graphics device directly (well, almost), and perform basic drawing routines. In this case, we override the ScrolledWindow's OnDraw method, and use the DC provided to display our image. OnDraw will be called by wxWidgets each time a portion of the window needs to be refreshed.

> /**
> * Override the OnDraw method so we can draw the bitmap.
> */
> public override void OnDraw(DC dc)
> {
> // Draw the bitmap onto the device context
> dc.DrawBitmap(m_bitmap, 0, 0 , false);
> }
> }

We now have a class for displaying and scrolling an image, and are now almost ready to put everything together.

### Scrolling a Window with Sizers Inside

We still need a mechanism for listing the images with the Thumbnail class created earlier.

Once again we will use a wx.ScrolledWindow, but we won't be setting the scrollbars manually this time. Instead, we will give the ScrolledWindow a sizer, then let the sizer set the scrollbars for us.

Here is the start of our ImageList class. The class holds a reference to a ImageViewer, so that when a thumbnail is clicked, the image can be displayed.

/**
* Class to display a list of images. The scrolled window helps us with
* managing scrollbars when the window's contents are too large to be
* displayed.
*/
public class ImageList : ScrolledWindow
{
private ImageViewer m_viewer;
/**
* Sets up the image list.
*
* The base class is initialized with a default window position,
* and a static size of width = 150, The '-1' for height means that
* the height of the window is unconstrained.
*/
public ImageList(Window parent, ImageViewer viewer)
: base(parent, -1, wxDefaultPosition, new Size(140, -1))
{
m_viewer = viewer;
// A flex grid sizer will be used to align the images horizontally.
// This is used, because the flex grid allows for entries to be
// of various sizes.
FlexGridSizer sizer = new FlexGridSizer(1, 0, 0);
// For now, our sizer will remain empty until images are loaded.
Sizer = sizer;
// Initialize the scrollbars
SetScrollbars(0, 1, 0, 0);
}

Even though the images will be listed vertically, we use a FlexGridSizer here to enable the thumbnails to vary in size.

We now provide a mechanism for listing all the images. The ListImages() method takes an array of images, then creates Thumbnail objects to display the images.

The Thumbnail delegate is tied to a method in the class, and the Thumbnails are added to the FlexGridSizer. In addition to this, a StaticLine is used to help create a small visual gap between each Thumbnail.

> /**
> * Lists the images in the given list of image file names.
> */
> public void ListImages(string[] images)
> {
> // Clear our list
> ClearImages();
> // Add the images
> foreach(string imageFile in images) {
> // Create a thumbnail will display the image
> Thumbnail thumb = new Thumbnail(this, imageFile);
> // Hook in our event handler
> thumb.ThumbnailClicked =
> new Thumbnail.ThumbnailClick(OnThumbnailClicked);
> // Add the bitmap button to the sizer.
> Sizer.Add(thumb, 1, Stretch.wxEXPAND);
> // A horizontal static line is used to help seperate the
> // images in the list.
> StaticLine line = new StaticLine(this, -1, wxDefaultPosition,
> wxDefaultSize,
> StaticLine.wxLI_HORIZONTAL);
> // The static line is added with a small border on the bottom
> Sizer.Add(line, 0, Stretch.wxEXPAND | Direction.wxBOTTOM, 3);
> // Fit inside tells the scrolled window to reset the scrollbars,
> // so that the entire window's contents can be scrolled.
> FitInside();
> }
> m_viewer.Bitmap = null;
> }

The magic behind getting the scrollbars set correctly using the sizer also happens here. When we call FitInside(), the scrolled window knows that the sizer's contents may be larger than the window, and the scrollbars are set appropriately.

Before listing the images, we also cleared the list. The Sizer.Remove method is used for this.

> /**
> * Clear all the images in the list.
> */
> private void ClearImages()
> {
> // This will remove all the windows (images) that are in the sizer.
> //
> // The 'true' parameter tells the sizer to delete the windows as
> // well. If 'false' were given, we would have to manually do
> // this.
> Sizer.Clear(true);
> }

Finally, we define the event handler for when a Thumbnail is clicked. Here, we simply give the Thumbnail's image to the ImageViewer, and give the main frame a new title.

> public void OnThumbnailClicked(string fileName, wx.Bitmap bmp)
> {
> m_viewer.Bitmap = bmp;
> // Set the main frame's title
> Parent.Parent.Title = fileName;
> }
> }

Now we have everything we need to start listing and displaying images. Next, we put everything together in our main frame.

### Splitters for Splitting a Window

A wx.SplitterWindow is ideal for displaying the contents of two windows with a resize bar between them. In our main frame, we want to have both the ImageList and ImageViewer displayed beside each other; a SplitterWindow will be used to do this.

First we add a few more members to the frame, so that we can display images.

> // String to hold the directory we're browsing
> private string m_directory = "";
> // The image list
> private ImageList m_list;
> // The image viewer
> private ImageViewer m_viewer;

Inside the ImageViewFrame constructor, we add the code to create the controls, and split them.

> // The splitter window allows two child windows to be displayed
> // with a bar between them.
> //
> // When there is only one child in a Frame, the child will fill
> // that frame, hence we don't need a sizer in this frame.
> SplitterWindow split = new SplitterWindow(this, -1);
> // Create the image viewing control
> m_viewer = new ImageViewer(split);
> // Create our image list
> m_list = new ImageList(split, m_viewer);
> // We now split the window with the two children
> split.SplitVertically(m_list, m_viewer, 150);

Not too difficult at all. The SplitterWindow is created, and the windows that we want to split up use the SplitterWindow as a parent. SplitterWindow.Split() is then called with the two windows we want to split up, and a third parameter specifying the initial position of the resize bar that divides the windows.

Earlier, the ImageList set the frame's title when a new image was selected. This behavior would normally overwrite the frame's title, and the application's name wouldn't be visible. To avoid this, we override the frame's Title property, and add some additional logic.

> /**
> * Override the base Title, since we want to add some more intelligent
> * behaviour.
> */
> public override string Title
> {
> get { return base.Title; }
> set {
> string title = "ImageView";
> if (value != "")
> title += " (" + value + ")";
> base.Title = title;
> }
> }
> }
> }

Now, what are we missing? Right, we're still not displaying any images. Just one final touch.

### Selecting a Directory

wxWidgets provides several dialog box classes, known as 'Common Dialogs'. The common dialogs are used for opening/saving files, choosing fonts and colors, and a variety of other tasks, including selecting a directory.

Not only do these dialogs make several tasks much easier, they also provide the user with a consistent interface, no matter what the application.

We will be using the wx.DirDialog to ask the user for a directory, then grab a list of image files in that directory to be passed to the ImageList for listing.

> DirDialog dlg = new DirDialog(this, "Choose an image directory",
> m_directory);
> if (dlg.ShowModal() == Dialog.wxID_OK) {
> m_directory = dlg.Path;
> // List the images
> string[] files = Directory.GetFiles(m_directory, "*.jpg");
> m_list.ListImages(files);
> Title = dlg.Path;
> }

By calling ShowModal() on our dialog, the dialog will be displayed in such a way that no other window in our application can interact with the user. When the user clicks 'OK' or 'Cancel' in the dialog, ShowModal() will return, and return Dialog.wxID_CANCEL or Dialog.wxID_OK, depending on what the user clicked.

### Conclusion

We now have an application for listing and displaying images in a directory. There are several enhancements that would still need to be made if this were to be widely distributed, but I leave that for a more advanced tutorial, or as an excerise for the reader.

If you have any questions, comments, or suggestions about this tutorial or wx.NET, please contact me.

> Bryan Bulten

Copyright © 2003 Bryan Bulten