View RSS Feed

Development Team Blog

A Folder Selector With Initial Folder Setting

Rating: 3 votes, 5.00 average.
This week two developers asked me about a folderbrowser/selector. Over the years several solutions have been created (fully) using the Windows API. But as far as I know they all do not have the ability to set the focus on a certain folder shown in the tree. And that is what the developers wanted to have. I said it is "simple" to make a folder selector yourself. This blog tells you how you can create one yourself using Visual DataFlex code and a bit Windows API code.

The Dialog
I think it is good if our folder selector dialog would popup on command which means you should create a ModalDialog component. The dialog should contain a tree with folders and buttons to accept the folder and close the dialog. Very useful would be a toolbar with buttons to create a new folder or delete one. So start designing a dialog. For this go to Create New, Dialog / Lookup and select Modal Dialog. Enter a name like oFolderSelectorDialog and see a ModalDialog object created with two buttons. From the class palette drag a cCJToolBar (Menus and Toolbars) and a treeview (Base Controls) to the dialog. Name the treeview object oFoldersTreeView. If you like you can give the tool-bar already two buttons but later the full source code for the tool-bar is published in this blog and that source code includes buttons and code. Position the treeview at 18,5 and align the buttons created by the template with the right hand side of the treeview. Set the peAnchors property of the treeview to anAll so that the folders list nicely resizes if the user resizes the dialog. Make the dialog look like:



Read the Folder Structure
Now all the visual controls are in place the coding starts. No-one want to re-invent the wheel and you can borrow some code from the ImageGallery demo application in the Graphics Library. If you do not have this library yet you can use the link to download and install. Copy the package FileDateTime.Pkg from the functions sub-folder. From the code copy the oImageList object to the dialog and do not forget to copy the bitmaps named in the source code to the bitmaps folder of your workspace. Copy the property settings for TreeLinesState and ImageListObject. Then copy the Enum_List and the methods LocateTreeItem, LocateItem, LocateRootItem, LocateFolder, OnItemExpanding, OnItemCollapsed, ReadFolderData and OnCreateTree to your treeview. Remove the following code from the OnCreateTree method as the code is special to the ImageGallery example:

Code:
Get AddTreeItem (WindowsFolderPath (CSIDL_MYPICTURES, True)) 0 C_FOLDERITEM C_FOLDERBMP C_FOLDERBMP to hItem 
Set ItemChildCount hItem to 1

If (SysConf (SYSCONF_OS_MAJOR_REV) >= 6) Begin
    Get AddTreeItem (WindowsFolderPath (CSIDL_COMMON_PICTURES, True)) 0 C_FOLDERITEM C_FOLDERBMP C_FOLDERBMP to hItem 
    Set ItemChildCount hItem to 1                    
End
The code used the Windows API to retrieve your pictures folder and place this in the top of the folders list.
Also replace the following code:
Code:
Get ReadString of ghoApplication "Preferences" "CurrentFolder" "" to sCurrentFolder
If (sCurrentFolder <> "") Begin
    Send LocateFolder sCurrentFolder
End
with:
Code:
Get psInitialFolder to sInitialFolder
If (sInitialFolder <> "") Begin
    Send LocateFolder sInitialFolder
End
The above code makes it possible to set the initial folder of the folder selector to the folder we desire.

Select the Folder and Return
The dialog needs to have a method to show itself and wait for user input. For this change the oOk_Btn into a folder selection button:
Code:
Object oSelectButton is a Button
    Set Label to "&Select"
    Set Location to 200 153
    Set peAnchors to anBottomRight

    Procedure OnClick
        Handle hItem
        String sFolderPath
        
        Get CurrentTreeItem of oFoldersTreeView to hItem
        Get ItemFullPath of oFoldersTreeView hItem to sFolderPath
        Set psSelectedFolder to sFolderPath
        
        Send Close_Panel
    End_Procedure
End_Object
and add a new function named SelectFolder that initializes and opens the dialog. Make the function like:
Code:
Function SelectFolder Returns String
    String sSelectedFolder
    
    // Set to empty to avoid the function returns the selected folder
    // of a previous operation
    Set psSelectedFolder to ""        
    
    Send Popup_Modal
    
    Get psSelectedFolder to sSelectedFolder
    
    Function_Return sSelectedFolder
End_Function
The above code also requires you add a property to store the selected folder name into. Add this definition to the dialog using the following code:
Code:
{ Visibility = Private }
Property String psSelectedFolder
The meta-data tag above the property definition makes the property available for our coding but hidden for code-sense and for visual designers. The property is really a kind of hidden one.

Because the oOk_Btn created by the template is no longer there replace the On_Key statement with:
Code:
On_Key Key_Alt+Key_S Send KeyAction of oSelectButton
The Tool-Bar
The tool-bar on top of the view - as mentioned - needs to have a button to create a new folder and a button to remove a folder. The icons for the buttons are already available in your rich Visual DataFlex development environment. The coding of the tool-bar can be:
Code:
Object oFolderSelectorCommandBarSystem is a cCJCommandBarSystem
    Object oToolbar is a cCJToolbar
        Set pbCustomizable to False
        Set pbCloseable to False
        Set peStretched to stStretch
        Set pbGripper to False
        Set pbEnableDocking to False
        
        Object oNewFolderMenuItem is a cCJMenuItem
            Set peControlStyle to xtpButtonIconAndCaption
            Set psImage to "New16.bmp"
            Set psCaption to "New"
            Set psDescription to "New Folder"
            
            Procedure OnExecute Variant vCommandBarControl
                Send CreateFolder of oFoldersTreeView
            End_Procedure
        End_Object

        Object oDeleteFolderMenuItem is a cCJMenuItem
            Set psCaption to "Delete"
            Set psImage to "ActionDelete.ico"
            Set peControlStyle to xtpButtonIconAndCaption
            Set psDescription to "Delete Folder"
            Set pbActiveUpdate to True
            
            Procedure OnExecute Variant vCommandBarControl
                Send DeleteFolder of oFoldersTreeView
            End_Procedure
            
            Function IsEnabled Returns Boolean
                Boolean bEnabled
                
                Get MapsCurrentNodeToExistingFolder of oFoldersTreeView to bEnabled
                
                Function_Return bEnabled
            End_Function
        End_Object
    End_Object
End_Object
The property settings of the tool-bar make it not re-locatable (pbGripper), stretched to the full width (peStretched) and not closeable (pbCloseable) by the user. The tool-bar buttons show both the icon as well as the caption. For the IsEnabled function of the 2nd button we need to add the following code to the treeview.
Code:
Function MapsCurrentNodeToExistingFolder Returns Boolean
    Handle hItem
    String sItemFullPath
    Boolean bExist
    Integer iItemData
    
    Get CurrentTreeItem to hItem
    Get ItemData hItem to iItemData
    If (iItemData <= -1) Begin // Only accept folders for delete items that were created by the CreateFolder method
        Get ChildItem hItem to hChildItem
        If (hChildItem = 0) Begin
            Get ItemFullPath hItem to sItemFullPath
            Get PathIsDirectory sItemFullPath to bExist
        End
    End
    
    Function_Return bExist
End_Function
The method uses a function called PathIsDirectory which you can find in the code of this whitepaper that I wrote back in 2003. The other functions (FileDateTime for example) can be found in the Graphics Demo application.

NewFolder and DeleteFolder
Our dialog won't compile without the methods CreateFolder and DeleteFolder. Start with DeleteFolder as this is the easiest one. For a succesfull delete the method needs to get the path of the current tree item and after asking the user to confirmation the delete operation attempt to delete the folder with the Visual DataFlex command Remove_Directory.
Code:
Procedure DeleteFolder
    Handle hItem
    String sCurrentFolder
    Integer iItemData iAnswer            
    
    Get CurrentTreeItem to hItem
    Get ItemData hItem to iItemData
    If (iItemData <= -1) Begin // Only delete items that were created by the CreateFolder method
        Get ItemFullPath hItem to sCurrentFolder
        Move (YesNo_Box ("Do You Want To Delete '" - sCurrentFolder - "'?", "Confirm", MB_DEFBUTTON2)) to iAnswer
        If (iAnswer = MBR_Yes) Begin
            Remove_Directory sCurrentFolder
            Send DoDeleteItem hItem
        End
    End
End_Procedure
The code is really meant to delete a folder that is wrongly created by the CreateFolder method and not to delete other folders. Therefore it test if the folder was just added by CreateFolder via the tree-item's ItemData property. If the value of that property is equal to -1 delete can be attempted, else not.

The CreateFolder is a bit more complex. For the real folder creation you can make use of the Make_Directory command. Before Make_Directory command is executed the user can change the name. The default name for the new folder will be "New Folder". If there is already a folder with that name the code need to suggest an alternative name. Add the following code to the treeview:

Code:
Procedure CreateFolder
    Handle hItem hNewItem
    String sCurrentFolder sNewFolder sNewSubFolderName
    Boolean bExist
    Integer iCount
    
    Get CurrentTreeItem to hItem
    Get ItemFullPath hItem to sCurrentFolder
    If (Right (sCurrentFolder, 1) <> "\") Begin
        Move (sCurrentFolder - "\") to sCurrentFolder
    End
    Move "New Folder" to sNewSubFolderName
    Move (sCurrentFolder - sNewSubFolderName) to sNewFolder
    Get PathIsDirectory sNewFolder to bExist
    While (bExist)
        Increment iCount
        Move ("New Folder (" - String (iCount) - ")") to sNewSubFolderName
        Move (sCurrentFolder - sNewSubFolderName) to sNewFolder
        Get PathIsDirectory sNewFolder to bExist
    Loop
Now the new folder name is constructed it will be added to the treeview and the item will be opened for editing. To be able to edit the text you need to set the TreeEditLabelsState property of the treeview to true.The item will be marked via the ItemData member as created by the CreateFolder method. Add the following code to complete the CreateFolder method:

Code:
    Set ItemChildCount hItem to 1
    Get AddTreeItem sNewSubFolderName hItem -1 C_FOLDERBMP C_FOLDERBMP to hNewItem
    Send DoExpandItem hItem
    Set CurrentTreeItem to hNewItem
    Send Windows_Message TVM_EDITLABEL 0 hNewItem
End_Procedure
As you can see there is no Make_Directory yet. This is done in the treeview event that is fired when the user is ready with editing the folder name, OnEndLabelEdit. For this add the event to the treeview and paste the following code inside the method.

Code:
Integer iItemData iPos
String sItemFullPath sNewFolder

Get ItemData hItem to iItemData
If (iItemData = -1) Begin
    Get ItemFullPath hItem to sItemFullPath
    If (not (bWasCanceled)) Begin
        Move (RightPos ("\", sItemFullPath)) to iPos
        Move (Left (sItemFullPath, iPos) - sNewLabel) to sNewFolder
    End
    Else Begin
        Move sItemFullPath to sNewFolder
    End
    Set ItemData hItem to -2
    Make_Directory sNewFolder
End
As you can see the code again tests the ItemData member on -1. Then it finds the last folder separator character (backslash) and appends the new folder name to the path. Finally to avoid a user starts editing a label with a double click or F2 augment the OnBeginLabelEdit event with:

Code:
Integer iItemData

Get ItemData hItem to iItemData
If (iItemData <> -1) Begin
    Move True to bCancel
End
Floating Menu
To really complete the component add a context (floating) menu to the treeview object with the same features as the tool-bar. For this drag a cCJContextMenu from the class palette to the treeview and add two items: New Folder and Delete Folder. Use the same pictures used for the tool-bar buttons and copy/paste the OnExecute and IsEnabled event code into the objects of the context menu. Activate the context menu via the treeview OnItemRClick event.

Code:
Procedure OnItemRClick Handle hItem
    Send Popup of oFoldersContextMenu
End_Procedure
Accessing the Folder Selector
To select a folder you call the SelectFolder method and before this you can set the initial folder property. An example for calling is:
Code:
Set psInitialFolder of oFolderSelectorDialog to "C:\Projects\VDF\16.0\Visual Report Writer Integration Examples 1.5"
Get SelectFolder of oFolderSelectorDialog to sFolder


If you followed the steps above you now have a nice in Visual DataFlex written folder selector dialog. The source code is written while using Visual DataFlex 2010/16.1.
Attached Thumbnails Attached Thumbnails Click image for larger version. 

Name:	2011-05-21_205030.jpg 
Views:	3340 
Size:	31.2 KB 
ID:	4354   Click image for larger version. 

Name:	2011-05-22_092811.jpg 
Views:	3403 
Size:	59.3 KB 
ID:	4355  

Comments

  1. wila's Avatar
    Nice job Vincent, but I'm afraid that you have just reinvented the wheel as windows does come with a standard component for that. It is a Visual DataFlex shortcoming that it can't deal with the callback interface required to use that standard component and in my opinion creating your own component is not the right answer to replace a windows standard component.

    About 4 years ago I donated a dll component that does the same thing, it wraps a bit of code around the required functionality for the callback and uses the windows API:
    See here: http://support.dataaccess.com/Forums...en-File-Dialog

    The component is royalty free and can be used anywhere or by anyone.
    --
    Wil
  2. seanyboy's Avatar
    I've been meaning to write this for a while, so thanks Vincent.

    wila:
    in my opinion creating your own component is not the right answer to replace a windows standard compon ent.
    We want a version of a file (or folder) selector that ...

    - Is Jailed to a specified folder.
    - Doesn't allow someone to right-click a folder and kick off a context behaviour that unwittingly allows access to the rest of the machine.(1)

    So this is useful for us.

    (1) e.g. Right Click folder and click Open.
  3. Anders Ohrt's Avatar
    I agree with Wil; this might be fine as an exercise for working with the WinAPI but it's pretty annoying when applications decide to reimplement common controls since they never work as well as the MS standard ones.

    Wil: Very nice, any change of getting source for the DLL?