Chapter 26. Constructing an Aesthetic Texture Browser Control

 

If builders built buildings the way programmers wrote programs, then the first woodpecker that came along would destroy civilization.

 
 --Weinberg’s Second Law

The number of art assets used in the majority of games today can be anywhere from thousands to hundreds of thousands. These assets can be used in numerous places throughout a variety of in-game environments, and typically, the level designers are in charge of determining which assets go where. Some game studios build their own level editors that can manipulate world geometry and handle the placement and scripting of entities. These editors typically offer the ability to select an arbitrary mesh or primitive and assign a texture asset to the geometry. When you have thousands of textures available, designers are more productive if the editor is able to display a thumbnail preview of the different textures available for an environment instead of a textual listing. It is much more appealing to scroll through a collection of texture thumbnails than to scroll through a listing of filenames that might not even describe the contents in an adequate fashion. Texture browsing has its place in a variety of tools, but the most common place to offer it is within a world editor.

This chapter is geared toward building a control that offers texture or image browsing from both local image files and Managed Direct3D Texture resources. The System.Drawing namespace supports a variety of image formats, but some texture formats, such as DDS, are not supported unless a custom loader is written or the data is loaded through Managed Direct3D. The control will display textures that have been resized to fit within a thumbnail control, with support for both single and multiple selection of texture thumbnails. Each thumbnail will have a label for the filename and a label for the dimensions of the original image. All thumbnails will sit within a parent container control.

Swappable Loader Interface

One of the goals outlined for this component is the ability to switch the loader that processes the image files. While Managed Direct3D and Windows GDI+ are the only loaders supported by this chapter, it would be advantageous to design the component so that any loader implementing the appropriate interface could be plugged into the component. This component makes use of an interface and abstract class to define the common interface of all image loaders so that they can be swapped in and out.

The interface provides two Load methods, which are used to create a Bitmap object from an image file on the hard drive or from a memory stream. Some loaders may also require a handle to a resource, such as a window handle for the Direct3D loader, so this interface provides the ContextHandle property to support this requirement.

The following code defines the base loader interface and abstract class.

public interface IAbstractLoader
{
    System.IntPtr ContextHandle
    {
        get;
        set;
    }
   Bitmap Load(string fileName);
   Bitmap Load(MemoryStream stream);
}

With the loader interface defined, it is time to define the abstract loader class that implements the interface. This class stores the context handle of a control that certain loaders may need to operate correctly. The GDI+ loader does not use this, but Direct3D uses this handle to create a device with which the textures can be loaded. The following code defines the abstract loader class.

public abstract class AbstractLoader : IAbstractLoader
{
    private System.IntPtr _contextHandle;
    public System.IntPtr ContextHandle
    {
        get { return _contextHandle; }
        set { _contextHandle = value; }
    }
    public virtual Bitmap Load(string fileName)
    {
        return null;
    }
    public virtual Bitmap Load(MemoryStream stream)
    {
        return null;
    }
    protected AbstractLoader(System.IntPtr contextHandle)
    {
        _contextHandle = contextHandle;
    }
}

Note

You will need to reference System.IO for the MemoryStream object, as well as System.Drawing for the Bitmap object.

Windows GDI+ Loader

This is by far the easiest loader to implement, since it only takes a single line of code to load an image file. Windows GDI+ is available to all .NET applications without the reliance on any external dependencies, so it is an excellent choice when standard image formats like JPEG, BMP and GIF will do the job.

The following code implements the Windows GDI+ image loader.

public class NativeLoader : AbstractLoader
{
    public override Bitmap Load(string fileName)
    {
        try
        {
            return new Bitmap(Image.FromFile(fileName));
        }
        catch
        {
            return null;
        }
    }
    public override Bitmap Load(MemoryStream stream)
    {
        try
        {
            stream.Position = 0;
            return new Bitmap(Image.FromStream(stream));
        }
        catch
        {
            return null;
        }
    }
    public NativeLoader(System.IntPtr contextHandle) : base(contextHandle) {}
}

Note

You will need to reference System.IO for the MemoryStream object, as well as System.Drawing for the Bitmap object.

Managed Direct3D Loader

It is very easy to use Windows GDI+ to load images, unless, of course, the image format is not supported. Image formats that are not supported by Windows GDI+ require a different loader to process any unsupported image formats, so we are presented with two possibilities.

The first option is to write a custom loader that can read in the binary data and extract the image information. After the image information (for instance, the number of channels and the pixel data) has been extracted, the information would be used to build an Image object. This method can be somewhat problematic, especially if the image format comes in different variations, such as different compression options and 3D-specific values such as the Microsoft DirectDraw Surface (DDS) format for example. The custom loader can become quite large in size, and debugging could prove to be difficult.

The alternate solution, and the one covered in this chapter, is to wrap the built-in texture-loading capabilities of Microsoft Direct3D into a loader. There are a number of advantages to building this wrapper over building a custom loader from scratch. The biggest advantage is the time and money saved by not having to reinvent the wheel. Additionally, unless you have a custom image format that neither GDI+ nor Direct3D support, one that requires a custom loader, the formats supported by the TextureLoader utility of Managed Direct3D will almost always suffice for your project.

Now, before you jump into the code, it is important to address the main issue behind wrapping Managed Direct3D into a loader. The TextureLoader loads image files into Texture objects, not Image or Bitmap objects, meaning that a valid device must first be created before any loading can occur. This may sound like a daunting or cumbersome process, but it really isn’t all that bad. The main requirement for a device is a window handle, and because Windows defines a window as any control element, we can create a device using the window handle of our texture browsing control!

The loader will create a Managed Direct3D and bind it to the texture browser control, at which point the image files are loaded into texture resources. The image data is then extracted from these resources and saved into Bitmap objects. The device is released after the images are generated; at no point does any actual rendering occur.

The following code implements the Managed Direct3D device and image loader.

public class Direct3DLoader : AbstractLoader, IDisposable
{
    private Device _device;
    public override Bitmap Load(string fileName)
    {
        try
       {
            Bitmap result = null;
            if (_device == null)
            {
                PresentParameters presentParams = new PresentParameters();
                presentParams.Windowed = true;
                presentParams.SwapEffect = SwapEffect.Discard;
                _device = new Device(0,
                                     DeviceType.Reference,
                                     ContextHandle,
                                     CreateFlags.SoftwareVertexProcessing,
                                     presentParams);
            }
            using (Texture texture = TextureLoader.FromFile(_device, fileName))
            {
                using (GraphicsStream stream
                       = TextureLoader.SaveToStream(ImageFileFormat.Bmp, texture))
                {
                    result = new Bitmap(stream);
                }
            }
            return result;
        }
        catch
        {
            return null;
        }
    }
    public override Bitmap Load(MemoryStream stream)
    {
        try
        {
            Bitmap result = null;
            if (_device == null)
            {

                PresentParameters presentParams = new PresentParameters();
                presentParams.Windowed = true;
                presentParams.SwapEffect = SwapEffect.Discard;
                _device = new Device(0,
                                     DeviceType.Reference,
                                     ContextHandle,
                                     CreateFlags.SoftwareVertexProcessing,
                                     presentParams);
            }
            stream.Position = 0;
            using (Texture texture = TextureLoader.FromStream(_device, stream))
            {
                using (GraphicsStream processedStream
                       = TextureLoader.SaveToStream(ImageFileFormat.Bmp, texture))
                {
                    result = new Bitmap(processedStream);
                }
            }
            return result;
        }
        catch
        {
            return null;
        }
    }
    public Direct3DLoader(System.IntPtr contextHandle) : base(contextHandle) {}
    public void Dispose()
    {
        if (_device != null)
        {
            _device.Dispose();
            _device = null;
        }
    }
}

Note

You will need to reference System.IO for the MemoryStream object, as well as System.Drawing for the Bitmap object. Additionally, you will need to reference both Microsoft.DirectX and Microsoft.DirectX.Direct3D for the Managed Direct3D support.

Storing Texture Information

The texture browser will support three ways of loading an image as a texture: from an image file stored on the local hard drive, from raw binary data in memory, and from a preloaded bitmap. In order to provide a unified and straightforward way of accessing textures that are loaded in the browser, we need to create a container class that wraps the three load methods into a common interface. This interface will be known as a texture handle; it will store the appropriate data depending on the source of the image, and it will keep track of simple state information to support caching.

Texture handles will need to keep track of the image data and where the data originated from, so the enum defined below will be used to accomplish this.

public enum TextureHandleType
{
    FileSystem,
    Bitmap,
    RawData
}

The texture handle class has several different constructors, each with a different signature and parameter list. The texture handle type is set when one of the constructors is fired, and depending on the constructor, the appropriate type value is set.

If one of the constructors accepting a FileInfo object is used, then it is assumed that the image is being loaded from the hard drive, so the handle type will be set to TextureHandleType.FileSystem.

If a constructor is used that accepts a Bitmap object, it is assumed that the object contains the image data, and the handle type should be set to TextureHandleType.Bitmap.

Lastly, if a constructor is used that accepts a MemoryStream object, it is assumed that the memory stream contains the raw binary data of the image, and that the handle type should be set to TextureHandleType.MemoryStream.

Texture handles need a humanly readable way to distinguish themselves from one another. Since the image data does not have to come from the file system and can come directly from raw memory, the filename cannot be used as an identifier. It is for this reason that the Name property was introduced into the texture handle class.

The two boolean properties Generate and Loaded will be covered later in this chapter. They are flags describing whether or not the textures need to be regenerated, and whether or not the image itself was able to be loaded.

You should also notice that the TextureHandle class implements the IDisposable interface. This is because of the MemoryStream object, which the Dispose method will close if required.

The following code implements the texture handle class in its entirety.

public class TextureHandle : IDisposable
{
    private string _name;
    private FileInfo _file;
    private Bitmap _image;
    private MemoryStream _data;
    private bool _generate = true;
    private TextureHandleType _type;
    private bool _loaded;
    public string Name
    {
        get { return _name;}
        set { _name = value;}
    }
    public FileInfo File
    {
        get { return _file; }
        set { _file = value; }
    }
    public Bitmap Image
    {
        get { return _image; }
        set { _image = value; }
    }
    public MemoryStream Data
    {
        get { return _data; }
        set { _data = value; }
    }
    public bool Generate
    {
        get { return _generate; }
        set { _generate = value; }
    }
    public TextureHandleType Type
    {
        get { return _type; }
        set { _type = value; }
    }
    public bool Loaded
    {
        get { return _loaded; }
        set { _loaded = value; }
    }
    public TextureHandle(FileInfo file) : this(file, file.Name) { }
    public TextureHandle(FileInfo file, string name)
    {
        _file = file;
        _name = name;
        _type = TextureHandleType.FileSystem;
    }
    public TextureHandle(string filePath, string name)
    {
        _file = new FileInfo(filePath);
        _name = name;
        _type = TextureHandleType.FileSystem;
    }
    public TextureHandle(string name, Bitmap image)
    {
        _name = name;
        _image = image;
        _type = TextureHandleType.Bitmap;
    }
    public TextureHandle(string name, MemoryStream stream)
    {
        _name = name;
        _data = new MemoryStream(stream.ToArray());
        _type = TextureHandleType.RawData;
    }
    public TextureHandle(string name, byte[] data)
    {
        _name = name;
        _data = new MemoryStream(data);
        _type = TextureHandleType.RawData;
    }
    public void Dispose()
    {
        if (_data != null)
        {
            _data.Close();
            _data = null;
        }
    }
}

Note

You will need to reference System.IO for the MemoryStream and FileInfo objects, as well as System.Drawing for the Bitmap object.

Building the Thumbnail Control

With the loaders built, it is time to build the user interface controls. We will start with the thumbnail control, which will show the image to the user, along with a summarized amount of information. This control will operate as an independent and modular unit of code, and it will be used by the texture browser.

The processing of the image data is performed by the loaders, but the original texture size will generally be too big for the thumbnail display. The thumbnail control takes the image data of the associated texture handle, resizes it to the appropriate size, and then uses a resized copy of the original image for the display.

The thumbnail control also handles the visual appearance for selection. The constructor accepts a reference to the texture browser instance so that visual properties can be used and applied to the thumbnail control.

Aside from visual properties, the reference to the texture browser is used by the thumbnail control to relay event information back to the browser control.

The following code defines the thumbnail control and its related properties and functionality.

public partial class TextureThumbnail : UserControl
{
    private TextureBrowser _container;
    private TextureHandle _texture;
    private bool _selected;
    public TextureHandle Texture
    {
        get { return _texture; }
    }
    public bool Selected
    {
        get { return _selected; }
        set
        {
            if (_container == null)
                return;
            if (value)
            {
                this.BackColor = Color.Blue;
                FileNameLabel.BackColor = _container.BackgroundColorSelected;
                FileNameLabel.ForeColor = _container.ForegroundColorSelected;
                DimensionsLabel.BackColor = _container.BackgroundColorSelected;
                DimensionsLabel.ForeColor = _container.ForegroundColorSelected;
            }
            else
            {
                this.BackColor = SystemColors.ActiveCaption;
                FileNameLabel.BackColor = _container.BackgroundColor;
                FileNameLabel.ForeColor = _container.ForegroundColor;
                DimensionsLabel.BackColor = _container.BackgroundColor;
                DimensionsLabel.ForeColor = _container.ForegroundColor;
            }
            _selected = value;
        }
    }
    public TextureThumbnail(TextureBrowser container, TextureHandle texture)
    {
        InitializeComponent();
        _container = container;
        _texture = texture;
        if (_container != null)
        {
            this.FileNameLabel.BackColor = _container.BackgroundColor;
            this.FileNameLabel.ForeColor = _container.ForegroundColor;
            this.DimensionsLabel.BackColor = _container.BackgroundColor;
            this.DimensionsLabel.ForeColor = _container.ForegroundColor;
        }
        GenerateThumbnail();
        DisplayInformation();
    }
    private void GenerateThumbnail()
    {
        if (_texture.Image == null)
            return;
        int maxDimension = Math.Min(MaterialPreview.Width,
                                    MaterialPreview.Height);
        int resizedWidth = _texture.Image.Width;
        int resizedHeight = _texture.Image.Height;
        if (_texture.Image.Width > maxDimension ||
            _texture.Image.Height > maxDimension)
        {
            if (_texture.Image.Width > _texture.Image.Height)
            {
                resizedWidth = maxDimension;
                resizedHeight = (int)(_texture.Image.Height *
                                      maxDimension / _texture.Image.Width);
            }
            else
            {
                resizedWidth = (int)(_texture.Image.Width *
                                     maxDimension / _texture.Image.Height);
                resizedHeight = maxDimension;
            }
        }
        MaterialPreview.Image = new Bitmap(_texture.Image,
                                           resizedWidth,
                                           resizedHeight);
    }
    private void DisplayInformation()
    {
        if (_texture.Image != null)
        {
            this.FileNameLabel.Text = _texture.Name;
            this.DimensionsLabel.Text = String.Format(CultureInfo.CurrentCulture,
                                                      "{0} × {1}",
                                                      _texture.Image.Size.Width,
                                                      _texture.Image.Size.Height);
        }
    }
    private void ToggleSelection()
    {
        if (_container != null)
            _container.PerformSelect(this);
    }
    private void MaterialPreview_MouseClick(object sender, MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Left)
        {
            ToggleSelection();
        }
        else if (e.Button == MouseButtons.Right)
        {
            if (_container != null)
                _container.PerformRightClicked(this);
        }
    }
    private void MaterialPreview_MouseDoubleClick(object sender, MouseEventArgs e)
    {
        if (_container != null)
            _container.PerformActivated(this);
    }
}

Figure 26.1 shows the texture thumbnail control shown in the designer. There are two labels for the filename and dimensions, as well as a picture box in the middle to display the resized image.

Screenshot of the thumbnail control in design mode.

Figure 26.1. Screenshot of the thumbnail control in design mode.

Handling Custom User Events

The various notifications raised by the thumbnail and texture browser controls are going to need a way to reach the application consuming them, so we need to provide events that the consuming application can tie into.

Basically, all of the events provided by the controls will send the same information, so we can define a single class to hold the event arguments that will be sent to the various event delegates. The different events will be covered in the next section when the viewer control is discussed.

The event arguments class only tracks a single texture handle instance, which is a reference to the texture handle associated to the event being executed.

The following code defines the event arguments class for the controls.

public class TextureBrowserEventArgs : EventArgs
{
    private TextureHandle _texture;
    public TextureHandle Texture
    {
        get { return _texture; }
        set { _texture = value; }
    }
    public TextureBrowserEventArgs(TextureHandle texture)
    {
        _texture = texture;
    }
}

Building the Viewer Control

The viewer control is fairly straightforward. It is basically a user control with a panel that contains the thumbnail controls. This control determines the spacing and positioning of the thumbnails, handles notification events, and exposes appearance and functional settings. It also manages the loading, displaying, and caching of thumbnails using the appropriate loader. The following source code describes the texture browser control in its entirety. The source code listing is somewhat lengthy, one reason why I will not comment much on each piece individually, but the full source is needed to fully enable you to understand the control without referring to the Companion Web site. The source code on the Web site is fully commented if there is a specific piece that you wish to fully investigate.

public partial class TextureBrowser : UserControl
{
    public enum LoaderType
    {
        Native,
        Direct3D
    }
    public enum SelectionMode
    {
        Single,
        Multiple
    }

    public event EventHandler<TextureBrowserEventArgs> TextureSelected;
    public event EventHandler<TextureBrowserEventArgs> TextureDeselected;
    public event EventHandler<TextureBrowserEventArgs> TextureActivated;
    public event EventHandler<TextureBrowserEventArgs> TextureRightClicked;

    private LoaderType _loader = LoaderType.Native;

    private SelectionMode _selection = SelectionMode.Single;

    private int _margin = 5;
    private bool _cacheImages;

    private Color _canvasColor = SystemColors.ControlDark;
    private Color _backgroundColor = SystemColors.Control;
    private Color _foregroundColor = SystemColors.ControlText;
    private Color _backgroundColorSelected = SystemColors.Highlight;
    private Color _foregroundColorSelected = SystemColors.HighlightText;

    private AbstractLoader _imageLoader;

    private Size _oldSize;
    private bool _rebuildCache = true;
    private bool _applyAppearance = true;

    private List<TextureHandle> _textures = new List<TextureHandle>();
    private List<TextureThumbnail> _thumbnails = new List<TextureThumbnail>();

    [CategoryAttribute("Texture Browser Settings"),
    DescriptionAttribute("Loader system to use when processing images")]
    public LoaderType Loader
    {
        get { return _loader; }
        set
        {
            if (_loader != value)
            {
                _rebuildCache = true;
                _loader = value;

                switch (_loader)
                {
                    case LoaderType.Native:
                    {
                        _imageLoader = new NativeLoader(this.Handle);
                        break;
                    }

                    case LoaderType.Direct3D:
                    {
                        _imageLoader = new Direct3DLoader(this.Handle);
                        break;
                    }
                }
            }
            else if (_imageLoader == null)
                _imageLoader = new NativeLoader(this.Handle);
            DisplayThumbnails();
        }
    }

    [CategoryAttribute("Texture Browser Settings"),
     DescriptionAttribute("Selection mode of the control")]
    public SelectionMode Selection
    {
        get { return _selection; }
        set { _selection = value; }
    }
    [CategoryAttribute("Texture Browser Settings"),
     DescriptionAttribute("Whether or not to cache loaded images")]
    public bool CacheImages
    {
        get { return _cacheImages; }
        set { _cacheImages = value; }
    }

    [CategoryAttribute("Texture Browser Settings"),
     DescriptionAttribute("Background color of the texture browser panel")]
    public Color CanvasColor
    {
        get { return _canvasColor; }
        set
        {
            _canvasColor = value;
            ThumbnailPanel.BackColor = _canvasColor;
        }
    }

    [CategoryAttribute("Texture Browser Settings"),
     DescriptionAttribute("Background color of the thumbnail control")]
    public Color BackgroundColor
    {
        get { return _backgroundColor; }
        set
        {
            _backgroundColor = value;
            _applyAppearance = true;
            DisplayThumbnails();
        }
    }

    [CategoryAttribute("Texture Browser Settings"),
     DescriptionAttribute("Foreground color of the thumbnail control")]
    public Color ForegroundColor
    {
        get { return _foregroundColor; }
        set
        {
            _foregroundColor = value;
            _applyAppearance = true;
            DisplayThumbnails();
        }
    }

    [CategoryAttribute("Texture Browser Settings"),
    DescriptionAttribute("Background color of selected thumbnails")]
    public Color BackgroundColorSelected
    {
        get { return _backgroundColorSelected; }
        set
        {
            _backgroundColorSelected = value;
            _applyAppearance = true;
            DisplayThumbnails();
        }
    }

    [CategoryAttribute("Texture Browser Settings"),
    DescriptionAttribute("Foreground color of selected thumbnails")]
    public Color ForegroundColorSelected
    {
        get { return _foregroundColorSelected; }
        set
        {
            _foregroundColorSelected = value;
            _applyAppearance = true;
            DisplayThumbnails();
        }
    }

    public TextureBrowser()
    {
        InitializeComponent();
        this.Loader = LoaderType.Native;
    }

    public void AddTexture(TextureHandle texture)
    {
        if (texture != null)
        {
            _textures.Add(texture);
            _rebuildCache = true;

            DisplayThumbnails();
        }
    }

    public void AddTextures(TextureHandle[] textures)
    {
        foreach (TextureHandle texture in textures)
        {
            AddTexture(texture);
        }
    }

    public void AddTextures(List<TextureHandle> textures)
    {
        foreach (TextureHandle texture in textures)
        {
            AddTexture(texture);
        }
    }

    public void RemoveTexture(TextureHandle texture)
    {
        if (texture != null)
        {
            _textures.Remove(texture);
            _rebuildCache = true;
            DisplayThumbnails();
        }
    }

    public void RemoveTextures(TextureHandle[] textures)
    {
        foreach (TextureHandle texture in textures)
        {
            RemoveTexture(texture);
        }
    }

    public void RemoveTextures(List<TextureHandle> textures)
    {
        foreach (TextureHandle texture in textures)
        {
           RemoveTexture(texture);
        }
    }

    public void GenerateTexture(TextureHandle texture)
    {
        try
        {
            texture.Loaded = false;

            switch (texture.Type)
            {
                case TextureHandleType.FileSystem:
                {
                    texture.Image = _imageLoader.Load(texture.File.FullName);
                    break;
                }

                case TextureHandleType.RawData:
                {
                    texture.Image = _imageLoader.Load(texture.Data);
                    break;
                }

                case TextureHandleType.Bitmap:
                {
                    // Do nothing, data already there
                    break;
                }
            }

            if (texture.Image != null)
            {
                texture.Loaded = true;

                if (_cacheImages)
                {
                    texture.Generate = false;
                }
            }
        }
        catch (System.OutOfMemoryException)
       {
            throw;
        }
    }

    public void GenerateTextures(TextureHandle[] textures)
    {
        foreach (TextureHandle texture in textures)
        {
            GenerateTexture(texture);
        }
    }

    public void GenerateTextures(List<TextureHandle> textures)
    {
        foreach (TextureHandle texture in textures)
        {
            GenerateTexture(texture);
        }
    }

    public TextureHandle FindTexture(string name)
    {
        TextureHandle result = null;

        foreach (TextureHandle texture in _textures)
        {
            if (texture.Name.Equals(name))
            {
                result = texture;
                break;
            }
        }

        return result;
    }

    public TextureHandle FindTexture(FileInfo file)
    {
        return FindTexture(file, false);
    }
   public TextureHandle FindTexture(FileInfo file, bool fullPath)
    {
        TextureHandle result = null;

        foreach (TextureHandle texture in _textures)
        {
            if (fullPath)
            {
                if (texture.File.FullName.Equals(file.FullName))
                {
                    result = texture;
                    break;
                }
            }
            else
            {
                if (texture.File.Name.Equals(file.Name))
                {
                    result = texture;
                    break;
                }
            }
        }

        return result;
    }

    public void SelectAll()
    {
        foreach (TextureThumbnail thumbnail in _thumbnails)
        {
            thumbnail.Selected = true;
            if (TextureSelected != null)
                TextureSelected(this,
                                new TextureBrowserEventArgs(thumbnail.Texture));
        }
    }

    public void DeselectAll()
    {
        DeselectAll(null);
    }
    private void DeselectAll(TextureThumbnail skip)
    {
        foreach (TextureThumbnail thumbnail in _thumbnails)
        {
            if (skip != null && thumbnail.Equals(skip))
                continue;
            thumbnail.Selected = false;
            if (TextureDeselected != null)
                TextureDeselected(this,
                                new TextureBrowserEventArgs(thumbnail.Texture));
        }
    }

    private void DisplayThumbnails()
    {
        _thumbnails.Clear();

        if (_rebuildCache)
        {
            while (ThumbnailPanel.Controls.Count > 0)
            {
                TextureThumbnail th = (TextureThumbnail)ThumbnailPanel.Controls[0];
                th.Dispose();
                ThumbnailPanel.Controls.Remove(th);
            }

            foreach (TextureHandle texture in _textures)
            {
                if (texture.Generate)
                {
                    GenerateTexture(texture);
                }

                if (texture.Loaded)
                {
                    TextureThumbnail thumbnail = new TextureThumbnail(this,
                                                                      texture);
                    _thumbnails.Add(thumbnail);
                }
            }
                _rebuildCache = false;
            }
            else
            {
                foreach (TextureThumbnail thumbnail in ThumbnailPanel.Controls)
                {
                    _thumbnails.Add(thumbnail);
                }
 
                ThumbnailPanel.Controls.Clear();
            }
 
            int numberHorizontal = -1;

            foreach (TextureThumbnail thumbnail in _thumbnails)
            {
                if (numberHorizontal < 0)
                {
                    // determine how many thumbnails can be displayed on one row
                    numberHorizontal = (int)(ThumbnailPanel.Width / (thumbnail.Width != 0
                                                                ? thumbnail.Width : 1));

                    if (numberHorizontal <= 0)
                        numberHorizontal = 1;
                }

                thumbnail.Left = _margin + (thumbnail.Width + _margin)
                                                    * (ThumbnailPanel.Controls.Count %
                                                                    numberHorizontal);
                thumbnail.Top = _margin + (thumbnail.Height + _margin)
                                                   * (ThumbnailPanel.Controls.Count /
                                                                   numberHorizontal);
                ThumbnailPanel.Controls.Add(thumbnail);
            }

            if (_applyAppearance)
            {
                foreach (TextureThumbnail thumbnail in ThumbnailPanel.Controls)
                {
                    if (thumbnail.Selected)
               {
                    thumbnail.BackColor = BackgroundColorSelected;
                    thumbnail.ForeColor = ForegroundColorSelected;
                }
                else
                {
                    thumbnail.BackColor = BackgroundColor;
                    thumbnail.ForeColor = ForegroundColor;
                }
            }

            _applyAppearance = false;
        }
    }

    internal void PerformSelect(TextureThumbnail thumbnail)
    {
        switch (_selection)
        {
            case SelectionMode.Single:
                {
                    DeselectAll();
                    thumbnail.Selected = true;
                    if (TextureSelected != null)
                        TextureSelected(this,
                                  new TextureBrowserEventArgs(thumbnail.Texture));
                    break;
                }

            case SelectionMode.Multiple:
                {
                    if (Control.ModifierKeys == Keys.Control)
                    {
                        thumbnail.Selected = !thumbnail.Selected;

                        if (thumbnail.Selected)
                        {
                            if (TextureSelected != null)
                                TextureSelected(this,
                                new TextureBrowserEventArgs(thumbnail.Texture));
                        }
                        else
                        {
                            if (TextureDeselected != null)
                                TextureDeselected(this,
                                  new TextureBrowserEventArgs(thumbnail.Texture));
                        }

                        break;
                    }
                    else
                    {
                        DeselectAll();

                        thumbnail.Selected = true;

                        if (TextureSelected != null)
                            TextureSelected(this,
                                new TextureBrowserEventArgs(thumbnail.Texture));
                    }
                    break;
                }
        }
    }

    internal void PerformActivated(TextureThumbnail thumbnail)
    {
        if (TextureActivated != null)
            TextureActivated(this, new TextureBrowserEventArgs(thumbnail.Texture));
    }

    internal void PerformRightClicked(TextureThumbnail thumbnail)
    {
        if (TextureRightClicked != null)
            TextureRightClicked(this,
                                new TextureBrowserEventArgs(thumbnail.Texture));
    }

    private void ThumbnailPanel_MouseClick(object sender, MouseEventArgs e)
    {
        DeselectAll();
    }
    private void TextureBrowser_Resize(object sender, System.EventArgs e)
    {
        if (_oldSize != ThumbnailPanel.Size)
        {
            _oldSize = ThumbnailPanel.Size;
            this.DisplayThumbnails();
        }
    }
}

Figure 26.2 shows the texture browser control in design mode. The control is now complete, which means that we can start consuming it in our applications. The next section shows how to use it.

Screenshot of the texture browser control in design mode

Figure 26.2. Screenshot of the texture browser control in design mode

Using the Control

Using the new control is very easy. The first thing you want to do is add a reference to the control assembly. The next step is to add the texture browser control into your toolbox. You can do this by right-clicking on the toolbox and selecting the Choose Items... option. The Choose Toolbox Items dialog will appear, showing all the assemblies loaded in the Global Assembly Cache (GAC). The control library is not registered in the GAC, so you will need to click the Browse button and navigate to the assembly of the library. An entry called TextureBrowser will be selected in the list. You should now be able to click OK and see the control appear in your toolbox.

You can now drag the texture browser control from the toolbox onto your form. Resize the control to your liking and then go to its properties. Aside from the normal properties that are available for all Windows Forms controls, there is a new section called Texture Browser Settings that are control-specific settings. These settings are shown in Figure 26.3.

Properties for the texture browser control.

Figure 26.3. Properties for the texture browser control.

Change these settings to your liking, and then you can move onto the code for adding textures to the control.

Loading Textures from a Directory

Early in the chapter, I mentioned that the loaders support three different sources for image data. The most common source will be from files located in the file system, and the following code shows how to iterate through a directory and load the image files into the texture browser control. Be sure to reference the System.IO namespace.

DirectoryInfo directoryInfo = new DirectoryInfo(path);
if (directoryInfo.Exists)
{
    FileInfo[] files = directoryInfo.GetFiles();
    foreach (FileInfo file in files)
        TextureBrowserInstance.AddTexture(new TextureHandle(file));
}

Loading Textures from a MemoryStream

The second source type for loading textures is from raw binary data stored in a MemoryStream object. This type is useful when images are extracted from storage archives, pulled off of a network connection, or programmatically generated. The following code shows how to load an image into the texture browser control from memory. Be sure to reference the System.Drawing and System.IO namespaces.

Image image = Image.FromFile(@".MemoryImage.bmp");
MemoryStream stream = new MemoryStream();
image.Save(stream, ImageFormat.Png);
TextureBrowserInstance.AddTexture(new TextureHandle("My Image", stream));

Loading Textures from a Bitmap

The last source type provides the ability to use an existing bitmap image when creating a texture handle. This method does not require any additional loading, since the image data has already been loaded into the bitmap object. This approach is useful when retrieving embedded resource content from an assembly resource stream, or images that are programmatically generated. The following code shows how to load a texture from an existing bitmap.

Image image = Image.FromFile(@".MemoryImage.bmp");
TextureBrowserInstance.AddTexture(new TextureHandle("My Image", image));

Texture Browser Demo

The Companion Web site has the complete source code for the library presented in this chapter, along with a simple demo that utilizes it. The demo just loads images located in a folder with the application, but supports the ability to switch image loaders at runtime.

Multiple selection is enabled, and you just have to hold down the Ctrl key while selecting multiple thumbnails. A context menu is bound to the right-click event of the thumbnails to present a dialog that shows simple information about the image. Lastly, the Activated event, (double-click), for the thumbnails shows a simple message box where other functionality could be implemented in a real-world application. An example of real-world functionality might be the ability to double-click a thumbnail and have a selected model mesh reference the texture as its color map.

Figure 26.4 shows a screenshot of the texture browser demo application.

Screenshot of the demo application using the texture browser.

Figure 26.4. Screenshot of the demo application using the texture browser.

Conclusion

This chapter covered the construction of a reusable control that can display images in a visually appealing way, and can also manage the handling and notification of events associated with the control.

There are some places where refactoring could improve the overall design of the component, but the most notable place is the Managed Direct3D loader. The point of having pluggable interfaces is to decouple the reliance on external dependencies when a particular component is not in use. Consider the situation where it is preferred that an application consuming this control does not use the Managed Direct3D loader, but instead uses the native GDI+ version. Sure, the loader can be set to native, but the component is still referencing the managed DirectX assemblies. If the consuming application is launched on a system without these assemblies, then a File Not Found exception would be thrown during execution, even though the Managed Direct3D loader is never used.

This problem could be solved by compiling the abstract loader interface into a separate assembly, along with both loader types compiled into their own assemblies. The texture browser component and both loaders would reference the abstract loader interface, and the texture browser component would dynamically reference the appropriate loader at runtime using reflection.

With such a problem, you are probably wondering why the component was not designed to accommodate this decoupling in the first place. Reflection and plug-in–based architectures would be the ideal “best practice” way to design the component, but this chapter is meant to cover how to build the component itself, hence why the component was designed the way it is. Removing this dependency on Direct3D can be done two ways, both very easily. You can exclude the Direct3D loader code from the texture browser component and recompile it; do not forget to remove the assembly references as well. Or you can refactor the component to support a plugin–based architecture, which is covered in Chapter 38, “Designing an Extensible Plugin-Based Architecture.”

Overall, this chapter presents a solid and reusable component that can be employed in a number of tools.

 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.253.223