Click here to Skip to main content
15,883,978 members
Articles / Programming Languages / C#

GUI-FileDialog Designer

Rate me:
Please Sign up or sign in to vote.
4.76/5 (5 votes)
9 Jan 2020CPOL15 min read 7.4K   647   11  
Design unique looking FileDialogs you can include in your own applications

Click to enlarge image

The Balad of the Unloved FileDialog

There once was a FileDialog
Who took his task most seriously.
Through the files with never a backlog
He’d proven his navigability.
To others he seemed a pedagogue,
Who with sneers they glared most cynically.
Though their files he kept in catalog
His masters treated him neglectfully.
Fattened records he fetched as might a trained bird-dog
But still they treated him most odiously.

His appearance was clean and never unkempt,
For he presented himself unpretentiously,
Still their regards for him neared mild contempt
Though requests he delivered impeccably
Reporting what was sought on his first attempt
The faithful FileDialog tried ever more diligently
And though Fickle Fate he never did tempt
On he strove for love and sensitivity
Without which he’d never be content
This with ardent fire he swore vociferously

Then one day he realized that his hairstyle
Was perceived most unfashionably
No longer the demur and stately bibliophile
He would ever again want to be
Since his demure aspect was so reviled
Determined he was to cast off his modesty
His old self he molted and left for erstwhile
As he shed his stale clothing like a priest does heterodoxy
Conventions soon he broke with a smile
And among deriding peers he walked with eccentricity

Dressing in stripes and polka-dot
He changed it up episodically
A hat he donned then as an after thought
Off he doffed it very luxuriously
Pulling on chameleon like culottes
He pranced and danced lackadaisically
As if he’d won the Lottery-Jackpot
A great many new friends arrived spontaneously
But it was no wealth of his they sought
For they only loved his endless mutability.

GUI-FileDialog

Granted, Visual Studio FileDialogs are work-horses that do what they're supposed to. That may be all well and good but there are times when you don't need the versatility of a stately File Explorer Dialog window to interact with the Users of your cool looking App or game. There's no greater mood-killer than a boring old FileDialog that brings you back to reality. When you're playing a game and you're lost in NeverLand, Narnia or Mordor, you never want to come back down to reality and interact with the same interface that reminds you of the Office or school work you're trying to get away from. If you're distracted living in an alter-world of pixillated slaughter and the long sought treasure is only five Yellow-Moonstones and a Pixie fight away, the last thing you want is to be reminded that you're not really the Fuzzy-Red-Fox you've worked so hard to become. Here's where the standard Visual Studio FileDialogs can stunt a player's imagination and drive her back to the mundane chores of reality instead of racking up points in that awesome game you put so much time and effort into. Getting players to try your app is hard enough without having to worry about chasing them away with the boring standard interface they use to save the homework they wish they didn't have to think about.

So here you have it, what you never knew you needed, a really cool way to create FileDialogs of endless variation. The GUI-FileDialog Designer app allows you to create FileDialogs using images you either draw yourself or download off the Internet. The only limit to this awesome creative tool is your imagination.

Have a look at this demo.

You can see that the versatility is not lacking. Even though users can't

  1. edit the names of files already on the hard-drive
  2. view Extra-Large Icons
  3. view file details (or reorder files in the list)

The GUI-FileDialog allows the developer to specify

  1. use any source image as the basis for their FileDialog
  2. select position, size, font and colors of each region independently
  3. single or multi-file mode
  4. and search pattern filters

So when you don't want all the features of a File Explorer type FileDialog, you might just want to play with a GUI-tar or GUI-nea Hen. And at other times, you'll discover that what you really need is a GUI-nea Pig... because sometimes, you just really need a GUI-nea Pig!

I've been working on this project for about a month and a half. Really thought it would only take me a week but that was wishful thinking. Adding features and debugging as I went along, I've used it in several of my other projects and have it running pretty well. There was an issue with the DirectoryTree not scrolling to the appropriate location when the Collapse/Expand buttons were pressed (or double-skipping whenever the User double-clicked and opened a file folder in the CurrentDirectoryContents region) that appear in the demo-video above but these issues have been fixed and I'm just too lazy to make another video that reflects those changes. So download the source-code above, load the .fdGUI example files and have a look for yourself.

Using the Code

As the video demonstrates, using the GUI-FileDialog and including it in your own code is very simple.

  1. You first design your GUI-FileDialog using the GUI-FileDialog Designer app
  2. Then add the FileDialog_Gui.cs file to your project through the Project-Add_Existing_Item menu-selection
  3. (optional) Add your .fdGUI file (GUI-FileDialog you designed) to your project's resources
  4. Declare an instance of the FileDialog you need in your code
  5. and load the .fdGUI file you've designed

The code shown below assumes the FileDialog_Gui.cs and your .fdGUI design file have been added to the project:

C++
FileDialog_GUI.OpenFileDialog_GUI ofd = new FileDialog_GUI.OpenFileDialog_GUI();
ofd.Title = "This GUI-nea Pig fetches files";
ofd.MultiSelect = false; // default setting
ofd.Filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*";
ofd.GUI_Load(Properties.Resources.FileDialog_GUI_nea_Pig);
if (ofd.ShowDialog() == DialogResult.OK)
{
    MessageBox.Show(ofd.FileName);
}

About the Code

The GUI-FileDialog inherits a PerPixelAlpha Form which allows your form to take the shape and appearance of its background Image. Unfortunately, as I have discovered, PerPixelAlpha Forms will not allow you to display any objects on the form. You can add TextBoxes and Labels all you want but your User won't be able to see them. This can be a problem when you try to use the form for anything that involves any of the copious myriads of objects, widgets and whatsits Microsoft provides for your interactive leisure.

Therefore, all commerce between the Monkey-in-the-Chair and the app has to be carried out using the mouse and two hidden TextBoxes that are Focused alternately depending on which region the User is busy with. In order to do this, the form is divided into several regions which can be easily and quickly detected whenever your Monkey interacts with the form by moving and clicking the mouse. Here, we rely on a SweepAndPrune algorithm which orders your sub-regions in two lists (horizontal and vertical) and partitions them into sequences of contiguous sub-regions. When you do a search for a point at (x, y), SweepAndPrune scans through the horizontal list of partitions, finds the one that contains the X requested and returns the list of regions that contain that value (if any such regions exist), then it does the same thing with the Vertical list and compares the results to return the sub-region that contains both the X and the Y of the point you're looking for.

That's SweepAndPrune, in a nut-shell.

In the case of the GUI-FileDialog, the SweepAndPrune class is the work-horse without which this project could not be possible (that and coffee ... lots of coffee). So the Form itself is first divided into 9 distinct regions and their associated ScrollBars:

C++
public enum enuRegions 
{ 
    Title, 
    CurrentDirectory, 
    DirTree, 
    ContentCurrentDirectory, 
    UserInput_Label, 
    UserInput_TextInput, 
    UserInput_Filter, 
    UserInput_Confirm, 
    UserInput_Cancel, 
    Resize, 
    _num 
};

The class classRegion defines the area each of these regions occupy on the form in the rectangle variable rec as well as Font, ForegroundColor and BackgroundColor. Each region is drawn separately and has an image of itself it keeps ready in the variable bmp. Title, CurrentDirectory and UserInput_Label are straightforward regions that do not interact with the User, but simply display information that is put to them. The Confirm and Cancel Regions are buttons that react when the Mouse moves over them or they are clicked by the User but by far the most complicated regions are DirectoryTree, CurrentDirectoryContents and UserInput_TextInput which we'll look into in a moment. The form's SweepAndPrune algorithm not only keeps track of these Regions but also any ScrollBars those regions may need and we'll have a look at those in turn.

DirectoryTree

The Directory Tree shows the User where in the tree of directories they are currently exploring. Each item in the DirectoryTree is both a classButton (that interacts with the User) and a classTreeItem that acts in a way similar to an element in a data tree by pointing to one parent and one, several or no child Tree Item elements. To build the tree, the app first starts in the current directory and recursively tracks its parents and ancestors until it reaches the root-directory while setting each Tree-Item's (sub-directory between the root and current directory) boolean Expanded variable to true. From there, it branches forward in a breadth-first-search (BFS) manner into all Expanded Tree Items while adding the contents of each expanded Tree-Item to their respective child-Tree-Items lists until the visible tree is complete. Since the User determines which Directory is expanded/collapsed as she goes along, every change made must be reflected on the screen but the process of adding Child-Tree-Items only occurs when a previously un-expanded Tree-Item is expanded and its contents needs to be loaded. The contents of Tree-Items are never deleted, nor are they revised to reflect changes made outside your app. Once this is complete, the algorithm counts how many ancestors each Tree-Item has and uses that value to determine each Tree-Item's horizontal tab location when it will later be drawn onto a bitmap.

Before the Directory Tree can be drawn however, an imaginary field is created onto which all the Tree-Items are positioned as if they were drawn onto a Bitmap. The size of this imagary field is then recorded in szBmp variable which is used by both of the DirectoryTree Region's Horizontal and Vertical scroll bars (more about the scroll-bars later). Once all the Tree-Items have been positioned in memory, the CurrentDirectory is used to determine what information needs to be drawn to the screen. After a bit of magic to determine whether or not either or both Scroll-Bars will need to be drawn (and cut into the space available for the Directory-Tree itself) a Bitmap is created that is four times the size of the space available for the Region on the output form (50% along all four edges). The Region's Rectangle variable recVisible tracks which part of the imagary field is on the screen while a Point variable ptOffset adjusts for the fact that the bitmap that is drawn is not the complete imaginary field but only a small portion of it. Which part of that Bitmap is drawn to the screen is a function of recVisible and ptOffset

C++
DrawDirTree();
Rectangle recVisible = new Rectangle(regDirTree.ptRecVisible_Offset.X,
                                        regDirTree.ptRecVisible_Offset.Y,
                                        regDirTree.recVisible.Width,
                                        regDirTree.recVisible.Height);
g.DrawImage(regDirTree.bmp, regDirTree.rec, recVisible, GraphicsUnit.Pixel);

Here, DrawDirTree(); ensures regDirTree's bmp is up to date (included MouseOver highlights). Then a rectangular region called recVisible is defined to source that region's bitmap. The Region's recVisible tracks which part of the Region's imaginary field is on the screen while this local recVisible tracks which part of the Region's bmp image is put to the screen depending on the current value of the Region's ScrollBars.

When the User scrolls along that region, the recVisible and ptOffset variables are altered so that the region's bmp (which only draws a small portion of the entire Directory Tree) can be sampled at the appropriate point in order to display the information requested on the screen. When the User scrolls beyond what is readily available in the bmp image (beyond 50% of the width of the Region's visible area defined by its rec variable), another Bitmap is drawn and the ptOffset value is reset and we start again.

As the User Expands and Collapses directories in the Directory Tree, the contents of newly expanded Tree-Items are searched on the Hard-Drive and added to their list of Child-Tree-Items. The DirectoryTree itself expands as the contents are added but they are not removed when the Directories are collapsed again. The data related to each sub-directory is retained once it has been retrieved but the appearance on the screen reflects the User's requests. Therefore, the imaginary field of Tree-Items in memory needs to be rebuilt and the Region's bmp redrawn whenever a change is made to any of the already visible Tree-Item's Expanded/Collapsed states.

Since the mouse is directly over any Expand/Collapse buttons the User may click, it is not necessary to locate that button in the DirectoryTree at that time, however, when the User DoubleClicks a FileFolder in the CurrentDirectoryContent Region and expects that folder to be Expanded, the location of the corresponding Tree-Item in the TreeDirectory must be retrieved in order to determine its location and set the ScrollBar's value to put that Tree-Item within the visible area. To do this a binary-tree keeps track of the Tree-Items and can be searched using FullPath names. In this way, when the User does expand a folder while in the CurrentDirectoryContent Region the Tree-Item is quickly retrieved, its location in the imaginary field determined and used to calculate the appropriate scrollBar Value which will place that Tree-Item on the screen for the User to see.

Current Directory Contents

The CurrentDirectoryContent Region is drawn in a fashion similar to the DirectoryTree but must take into consideration the Icon-Size (display style) the User has selected when creating the buttons for its imagary field. Once the buttons are positioned in the imaginary field, it is drawn using the same scheme as the Directory Tree. The file Icons are retrieved using the code seen here.

C++
Icon icon_File(string strPath)
{
    string strExtension = getExtensionFromPath(strPath);
    Icon icoRetVal = classFileIcons.Search(strExtension);

    if (icoRetVal == null)
    {
        try
        {
            icoRetVal = System.Drawing.Icon.ExtractAssociatedIcon(strPath);
        }
        catch (Exception)
        {
        }

        if (icoRetVal != null)
        {
            classFileIcons.Insert(cTI.fileInfo.Extension, ref icoRetVal);
        }
        else
            ;
    }

    return icoRetVal;
}

The classFileIcons holds a static binary-tree which is used to store the icons that have already been found. When an Icon is needed, the binary tree is first searched. Then if the Icon that is sought is not found there, it is extracted using the System.Drawing.Icon.ExtractAssociatedIcon() function native to Visual Studio and inserted into the tree for later retrieval.

UserInput_TextInput

This is the Region where Users can type the name of the file they want to load. Although the form can detect KeyDown and KeyUp events, two TextBoxes were used to interact with the User when any typing or keyboard operations are required. Focus is always shifted to either of these two TextBoxes. The first txtInput is focused when the User selects the UserInput_TextInput Region and the other is focused the rest of the time. The contents of the txtInput TextBox are set to the current directory whenever the directory changes, or the name of any file that the User selects in the CurrentDirectoryContent Region. Whenever the text content of this TextBox is altered, it's TextChanged event is triggered and its contents are reflected on the screen.

Each character that is drawn in the UserInput_TextInput Region is an instance of the classButton. Its image is stored in the class's binary-tree which has its root element reset to null whenever the Region's Font or Colors are changed. Using the TextRender.MeasureText() function provides too large an image size for individual alphanumeric characters that need to be squeezed closely together in order to give the appearance of continuous text and not a collection of disjointed images. Therefore, each character's image is first measured by making a string of 64 samples of the same character. That string is then measured and the width is divided by the number of samples to give a more accurate result.

C++
public static classCharImage get(char chrSearch)
{
    classCharImage cCI_RetVal = search(chrSearch);
    if (cCI_RetVal == null)
    {
        int intNum = 64;
        Size szTest = TextRenderer.MeasureText("".PadRight(intNum, chrSearch), fnt);
        Size szChar = new Size(szTest.Width / intNum + 1, szTest.Height);

        cCI_RetVal = new classCharImage();
        cCI_RetVal.chr = chrSearch;
        cCI_RetVal.bmp = new Bitmap(szChar.Width, szChar.Height);
        using (Graphics g = Graphics.FromImage(cCI_RetVal.bmp))
        {
            g.FillRectangle(brBackgroundIdle, 0, 0, szChar.Width, szChar.Height);
            g.DrawString(chrSearch.ToString(), fnt, brForegroundIdle, new Point(0, 0));
        }

        cCI_RetVal.bmpHighlight = new Bitmap(szChar.Width, szChar.Height);
        using (Graphics g = Graphics.FromImage(cCI_RetVal.bmpHighlight))
        {
            g.FillRectangle(brBackgroundHighlight, 0, 0, szChar.Width, szChar.Height);
            g.DrawString(chrSearch.ToString(), fnt, brForegroundHighlight, new Point(0, 0));
        }

        Insert(ref cCI_RetVal);
    }
    return cCI_RetVal;
}

In the code above, you can see that the character's image is first searched in the static binary-tree. When it is not located there, it is drawn for the first time since the most recent changes were made to its appearance and inserted into the tree before being returned to the calling function. As the screen-time of a FileDialog rarely exceeds the time it takes for your niece's guinea-pig to chew its way through a leaf of lettuce, the life of a non-static binary-tree would be short. Since a designer's apps are likely to use the same fonts and colors for their FileDialog's TextInput Regions (if they use more than one design) the static tree will still be valid the next time the User decides to save or load whatever adventure they've lost themselves in.

ScrollBars

ScrollBars are similar to Regions in that they are mapped out in the same SweepAndPrune field that defines the form as a whole but they are ScrollBars attached to a Region and not Regions in themselves. The classScrollBar has Minimum, Maximum and Value variables just as the ScrollBars native to the language and they behave in a way your Users will be used to and find intuitive. The Value changes affect the ptOffset Point variable mentioned earlier and when that Point variable's values exceed a given limit then the Region's bmp sample source image needs to be redrawn.

History

It took exactly one month to get to this point but it seems longer. Though there are plenty of features left to implement and improve, since I have recently started new projects on top of unfinished projects that I've been neglecting as they simmer on a back-burner, its good to be able to wash my hands of this one and go back to stuff I've left unattended for so long. You might be interested to know, there's a Picture Jigsaw Puzzle Generator game in the works. That project is advanced enough that at the end of my day I can relax and play it for a half hour. That'll probably be the next thing I publish so, until then, ski-doo safe, eh!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
CEO unemployable
Canada Canada
Christ Kennedy grew up in the suburbs of Montreal and is a bilingual Quebecois with a bachelor’s degree in computer engineering from McGill University. He is unemployable and currently living in Moncton, N.B. writing his next novel.

Comments and Discussions

 
-- There are no messages in this forum --