About/News |
This tutorial still drawas a pretty good picture of how to use wx.NET with C#. However, this page is not maintained. Refer to the API reference and the documentation on the wx.NET samples of use for more detailed and up-to-date info. ImageViewer TutorialIn this tutorial, we will create a simple image viewer using wx.NET.
The tutorial is included in both the binary and source
distributions of wx.NET. In the binary distributions the tutorial has already
been compiled. In both distributions the source code is under the 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. PrerequisitesDownload either a binary and source distribution of wx.NET. 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 ApplicationAt the heart of every wxWindows 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. /** Then to start the application, we create an instance of the application object, and call its Run() method. [STAThread] 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 BarFor 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. /** To add the newly created frame class, we add the following to our application's OnInit method: // Create the main frame 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 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 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 wxWindows will be able to associate actions on the items with the event handlers that we declare. // The File menu The next menu is the Help menu, with one item. // The Help menu 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, 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 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 EventsNOTE: The current version of wx.NET uses a macro-like system, similar to that used in wxWindows 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 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, wxWindows 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. /** We save the logic for the OnFileOpenDir() handler until later. For now it will just sit empty. /** 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. /** 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 wxWindows 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 wxWindows 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) 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 ControlsWe 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. /** The constructor will use these elements to construct a bitmap and display the bitmap with the file name as a label.
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. wxWindows 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 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 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 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 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 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 Our Thumbnail class will use a delegate to transfer events to anything that's listening. protected void OnBitmapButtonClicked(object sender, Event evt) 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 WindowTo display large images, we will need a window that can be used to scroll its contents. wxWindows provides the wx.ScrolledWindow class to do this. Here is the start of our ImageViewer class. /** 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. /** 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 wxWindows each time a portion of the window needs to be refreshed. /** 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 InsideWe 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. /** 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. /** 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) 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 WindowA 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.
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 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. /** Now, what are we missing? Right, we're still not displaying any images. Just one final touch. Selecting a DirectorywxWindows 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", 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. ConclusionWe 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
|