Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

The Explorer Imperative

4.60/5 (4 votes)
14 Jul 2011CPOL13 min read 20.4K   312  
File System Viewer with Font Zoom

starting-directory.png
Your Starting Directory

lists-of-files.png
List of Files in Folder

list-expanded.png
The "tList" expanded

HTML-file.png
HTML File showing Tool Tip

press-enter.png
Press <ENTER> to OPEN File

Introduction

Three Imperatives

Each man is born with three imperatives: the need to eat, the need to sleep and the need to Explore. Later on, other needs arise but that's the stuff for another story!

This article discusses the Imperative to Explore, ergo, "the Explorer Imperative". Actually it means 'an Explorer like program written in straight Imperative F#'; as opposed to Functional F#(FunF) or Object Oriented F#(OOF). Some more advanced F# programmers will say that to program in this style is an ImperFect and in-appropriate use of the F# Language, but I'm a newbie to the nth degree! My background is in IBM Main Frame languages, such as Bal, Cobol, Rexx and Dialog Manager.

I wrote this article because while I was learning F# syntax, a weird thing happened! I would write something down and it would just work, just like I wanted it to. The only times it didn't work, it was because I had the syntax wrong. And, incredably, it told me how to fix the error. I was amazed! I wanted to share my amazement with everyone. I humbly submit this work as an example of what can be done with a very small portion of this very powerful language.

Prerequisites

This sample requires F# Interactive(FSI.EXE) and/or the F# Compiler(FSC.EXE) which is available for download from the F# Development Center at FSharp.net, as well as a C# IDE of some sort. Also required are the WPF Libraries, included in Visual Studio 2010 Express, or another environment which will allow you to create an F# Project and add COM References, needed for an example of how to create a shortcut to the Desktop. All the examples work in both Interactive and Compiled Environments but Interactive is a lot more fun. And if you are good with command line you could use TLBIMP to create "Interop.IWshRuntimeLibrary.dll" from 'wshom.ocx'. If you have available an IDE and Compiler jusr add a Reference, selecting the "COM" tab and click IWshRuntimeLibrary. This can be done in an C# Project if you can't create an F# Project in your IDE.

Description

This sample contains an fsx script that can be interpreted by 'FSI.EXE' - F# Interactive or 'save as' an 'fs' file, add it to an F# Project. You can do either, your choice, I'm not including a project because it is far easier for those with the resources to create a Project than it is for one who is limited to FSI Interactive to try to get a compiled program to work in interpreted mode.

The Application, 'the Explorer Imperative'

This application started as a file system viewer with ZOOM but I soon discovered I could not stop adding functionality. It had taken over my mind. I was expecting Arnold to pop up. Finally it allowed me to stop but it said 'Go! Tell it on the Internet'.

The application is a WPF Window that is code-only. No XAML. The Window contains a single Grid with 3 Rows and 1 Coloum. The first Row contains a Textbox. The second Row contains only a Grid Splitter. This is used to re-size the height of the other two Rows. The third Row contains a Treeview in a Scrollviewer.

The Script version will run with or without a WPF EVENT LOOP. The WPF EVENT LOOP provides a base set of event responses for both MOUSE and KEYBOARD Events. If your application uses these responses then it needs a WPF EVENT LOOP. This application doesn't depend soley on the base set of responses, so most of the examples presented in this article work without it. This line, '//#load "WPFEventLoop.fsx"', can be uncommented if you 'bing' "WPFEventLoop" to find the Copywrited Micro Soft source code and either download it or type it in. The version I typed in had a reference to an obsolete function, 'rethrow', but fsi.exe told me to change it to 'reraise'. It worked great, but any EVENT HANDLER you write doesn't require it.Of course, the compiled program has has it's own WPF Event Loop, so it isn't loaded.

In the first section, We have the references that need to be added to the Project Definition when you define the project. I assume you are using an IDE that can create an F# Project. I therefore assume that you are not using the command line compiler. If you can only create a C# Project, You can use it to create Interop.IWshRuntimeLibrary.dll which will show in the project references as 'IWshRuntimeLibrary'. To add it, Right Click th references node in the Project Explorer ans click 'Add Reference'. Click the 'COM' Tab click any entry, then type 'w', hold down that key or the down arrow until you get down to 'Windows Script Host Object Model'. Select it. This will create a wrapper for the 'wshom.ocx' file in your File System and place it in your project. This is the preffered version, since the version that works on my File System nay not work on yours. Although this is last in the "#r's", it must be first in the references node and in the open statements.

F#
#light
    
#if INTERACTIVE
//#load "WPFEventLoop.fsx"
#I @"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0"
#r "PresentationCore.dll"
#r "PresentationFramework.dll"
#I @"C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319"
#r "System.dll"
#I @"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.5"
#r "System.Core.dll"
#I @"C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319"
#r "System.Xaml.dll"
#r "System.Xml.dll"
#I @"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0"
#r "WindowsBase.dll"
#r @"C:\Documents and Settings\Owner\My Documents\SharpDevelop Projects\
the Explorer Imperative\the Explorer Imperative\bin\Debug\Interop.IWshRuntimeLibrary.dll"
#endif

open IWshRuntimeLibrary
open System
open System.Collections.Generic
open System.ComponentModel
open System.Diagnostics
open System.IO
open System.IO.IsolatedStorage
open System.Linq
open System.Security.Permissions
open System.Text
open System.Windows
open System.Windows.Controls
open System.Windows.Controls.Primitives
open System.Windows.Data
open System.Windows.Documents
open System.Windows.Input
open System.Windows.Media
open System.Windows.Media.Imaging
open System.Windows.Navigation
open System.Windows.Shapes
open System.Windows.Threading
open System.Xaml

In the next section, the mainWindow and the objects that will be put into it are defined. At the end of this section is a block comment, delimited by a left parenthesise and an asterisk('*') at the beginning of rhe comment and an asterisk, followed by a right parenthesise at the end of the comment. Delete these 2 lines leaving the 3 lines of code in between the 2 lines and paste the code up to that point into F# INTERACTIVE. A WPF WINDOW will be displayed with the cHelpStr from the code below in the textbox. You can GRAB the SPLITTER BAR with MOUSE and Drag it down to make the textbox bigger. You can interact with the Window by using the MOUSE but that's about it.

If you have downloaded the source code either delete the entire comment or don't touch it. If the window is shown at this point, the tree won't get loaded, so the only thing new is, you can interact with the textbox, but that is all. You can delete the show function, but you will get some error messages, so just delete the entire comment.

F#
let dummyNode = null
let mutable copyArgs:string = null
let mutable pasteArgs:string = null
let mutable execArgs:string = null
//
// define 'ifl' here so streamreader and streamwriter
// will be using the same object. 
//
let mutable ifl = IsolatedStorageFile.GetUserStoreForAssembly() 
let mutable startInDir = @"C:\Documents and Settings\Owner\My Documents"
let mutable nextItem = new TreeViewItem()
let mutable focusItem = new TreeViewItem()
//
// Create the application's main window
let mainWindow = new Window()
mainWindow.Title <- "Loading the Explorer Imperative"
mainWindow.Width <- 860.0
mainWindow.Height <- 680.0
// Create the Grid
let myGrid = new Grid()
myGrid.MinWidth <- 40.0
myGrid.MinHeight <- 20.0
myGrid.HorizontalAlignment <- HorizontalAlignment.Stretch //Left
myGrid.VerticalAlignment <- VerticalAlignment.Top
myGrid.IsEnabled <- true
myGrid.Focusable <- true
//
// Define the Rows
//
let rowDef1 = new RowDefinition()
rowDef1.Height <- new GridLength(46.0)
let rowdefSplitterRow = new RowDefinition()
rowdefSplitterRow.Height <- new GridLength(10.0)
let rowDef = new RowDefinition()
rowDef.Height <- new GridLength(1.0)
let rowDef2 = new RowDefinition()
rowDef2.Height <- new GridLength(1.0, GridUnitType.Star)
// Add the rows to the Grid
myGrid.RowDefinitions.Add(rowDef1)
myGrid.RowDefinitions.Add(rowdefSplitterRow)
myGrid.RowDefinitions.Add(rowDef)
myGrid.RowDefinitions.Add(rowDef2)
// Create the text box and output instructions for splitter
let textBox1 = new TextBox()
let mutable cHelpStr =
 "PRESS the SHIFT and Tab Key together! Then PRESS the Down " +
 "Arrow Key. This sequence gives the KEYBOARD FOCUS to the " +
 "Splitter Bar and will increase the number of VISIBLE ROWS " +
 "of this TEXTB0X, that is, the textbox containing this text.\n\n" +
 "If you select Help>Contents from the MENU, the SPLITTER BAR " +
 "has KEYBOARD FOCUS so just Press the DOWN ARROW KEY.\n\n" +
 "Some information may span many lines. You can make the textbox " +
 "any size you want. You can even make it disappear. Press the " +
 "UP Arrow Key to Decrease the Visible Rows. To HIDE the TextBox " +
 "hold down the Up Arrow Key. The SPLITTER BAR can also be " +
 "GRABBED with the Mouse. If it is HIDDEN, HOLD DOWN the Shift " +
 "KEY and PRESS the TAB Key, then release the SHIFT Key and " +
 "PRESS the DOWN Arrow Key if you are in the EXPLORER panel. " +
 "Press TAB Key to go from the SPLITTER BAR to the EXPLORER " +
 "PANEL. TAB again to go to the TextBox. TAB TWICE to ESCAPE " +
 "the TextBox.\n\n" +
 "To activate the MENU place the Mouse Pointer on the " +
 "target item and PRESS the RIGHT MOUSE BUTTON. A 'MENU' " +
 "will 'Popup'with a Horizontal Alignment. If the MOUSE " +
 "Pointer is on the MENU, it will Capture the MOUSE when " +
 "the BUTTON is released. Some factors will cause the Menu " +
 "to be off of the selected Item. When this happens the MOUSE " +
 "will FOCUS on the Item it is on. You will need to move " +
 "the pointer and re-select the Item. If the MOUSE Pointer " +
 "is not on the Horizontal Menu a 'Context Menu' will popup. " +
 "The same MENU Selections are available on the contex menu " +
 "as are available on the Popup Menu.\n\n" +
 "Zoom works the same as Internet EXPLORER. Hold down the Control " +
 "key and roll the MOUSE Wheel.\n\n" +
 "To SCROLL Horizontally, 'nudge' the Wheel to the left or " +
 "to the right, then roll the Wheel. The SCROLLBAR will show" +
 "whether you can scroll vertically or horizontally.\n\n" +
 "     ---------------- END ----------------\n\n\n"
textBox1.Text <- cHelpStr
textBox1.FontSize <- 32.0
textBox1.AcceptsReturn <- true
textBox1.FontWeight <- FontWeights.Bold
textBox1.Width <- 1280.0
textBox1.TextWrapping <- TextWrapping.WrapWithOverflow
textBox1.TabIndex <- 2
// Tell the text box where to go in the Grid
Grid.SetRow(textBox1, 0)
// Create the gridSplitter and tell it where to go in the Grid
let myGridSplitter = new GridSplitter()
Grid.SetRow(myGridSplitter, 1)
myGridSplitter.HorizontalAlignment <- HorizontalAlignment.Stretch
myGridSplitter.VerticalAlignment <-  VerticalAlignment.Top
myGridSplitter.MinHeight <- 10.0
myGridSplitter.Height <- 10.0
myGridSplitter.TabIndex <- 0
// Create the scrollViewer and tell it where to go in the Grid
let myScrollViewer = new ScrollViewer()
myScrollViewer.VerticalAlignment <- VerticalAlignment.Top
myScrollViewer.VerticalScrollBarVisibility <- ScrollBarVisibility.Visible
myScrollViewer.HorizontalScrollBarVisibility <- ScrollBarVisibility.Hidden
myScrollViewer.CanContentScroll <- true
myScrollViewer.IsEnabled <- true
myScrollViewer.Focusable <- true
Grid.SetRow(myScrollViewer, 3)
// Create the root for the treeTrunk
let mutable myComputer = new TreeView() 
myComputer.IsEnabled <- true
myComputer.Focusable <- true
myComputer.TabIndex <- 1
// Create the treeTrunk
let treeTrunk = new TreeViewItem()
treeTrunk.Header <- "My Computer"
treeTrunk.FontSize <- 32.0 
treeTrunk.FontWeight <- FontWeights.Bold
// Add the elements to the Grid Children collection
let _ = myGrid.Children.Add(textBox1)
let _ = myGrid.Children.Add(myGridSplitter)
// graft the trunk onto the root, put it in
// the scrollViewer box add it to the grid orphanage
let _ = myComputer.Items.Add(treeTrunk)
let _ = myScrollViewer.Content <- myComputer
let _ = myGrid.Children.Add(myScrollViewer)
(* Delete or comment(//) this line and it's counterpart below

mainWindow.Content <- myGrid // Put the Grid in the Window

mainWindow.Show () // Show it to the user

mainWindow.Title <- "the Explorer Imperative" Show the Title

// This is the end of the Window definition

Delete or comment(//) this line and it's counterpart above *)

This function advances the cursor to the next item that begins with the character you type. The routine uses text input, rather than raw input so it is case sensitive. It wraps around, so the next hit may be before the current position. The method used is to find the first hit, then skip to the current position and begin the search for the next hit. If a hit is not found the current item retains focus. The hits are given focus as they occur. This means, the 'next' hit takes the focus away from the first hit, if it has it. This routine is not an eventhandler, but is a recursive    function. The 'rec' keyword specifies recursion, otherwise a recursive call will cause a compile time error. In other words, F# is not automatically recursive.

F#
let rec findThisChar(thisItem, currentItem, eKeyToString, foundit) =
  let mutable eKey:string = eKeyToString
  let mutable pItem:TreeViewItem = thisItem
  let mutable item:TreeViewItem = currentItem
  let mutable qItem = new TreeViewItem()
  let mutable whatFound:string = foundit
  for i in 0 .. (pItem.Items.Count) - 1 do
    qItem <- pItem.Items.[i]:?>TreeViewItem
    if whatFound <> "next" then
      if (qItem.Header.ToString().StartsWith(eKey) ) then
        if whatFound = "current" then
          whatFound <- "next"
          //printf "\nnext  %s\n" (qItem.Tag.ToString())
          Keyboard.Focus qItem|>ignore
        if whatFound = "nothing" then
          whatFound <- "first"
          //printf "\nfirst  %s\n" (qItem.Tag.ToString())
          Keyboard.Focus qItem|>ignore
      if qItem.Tag = item.Tag then
        whatFound <- "current"
      if qItem.IsExpanded then
        if whatFound <> "next"  then 
          whatFound <- findThisChar(qItem, item, eKey, whatFound)
  whatFound // return that what was found

This next routine is an eventhandler for the treviewitem 'treeTrunk'. It 'bubbles up' so it applies to all of the 'sub-nodes' of treeTrunk. This is the primary keyboard interface. It handles the cursor keys and the enter key. A detailed description of this routine will be provided in the near future.

F#
let keyUpDetected(e:KeyEventArgs) =
 try
  //printf "\nkey is %s\n" (e.Key.ToString())
  let mutable item = new TreeViewItem()
  if (e.Source :? TextBox) then
   e.Handled <- true
  elif (e.Source :? TreeViewItem) then
   item <- e.Source:?>TreeViewItem
   item.Focusable <- true
   item.IsEnabled <- true
   item.IsSelected <- true
   textBox1.Text <-  item.Tag.ToString()
  if e.Key = Key.Right then
   if File.Exists(item.Tag.ToString()) then
    try
     let filePopup = new Popup()
     filePopup.PlacementTarget <- e.Source:?>TreeViewItem
     filePopup.VerticalOffset <- -40.0
     filePopup.HorizontalOffset <- 20.0
     let mutable fileBox = new TextBox()
     fileBox.MinWidth <- 40.0
     fileBox.Text <- item.Header.ToString()
     fileBox.IsReadOnly <- true
     let mutable argBox = new TextBox()
     argBox.Margin <- new Thickness(20.0,2.0,2.0,2.0)
     argBox.MinWidth <- 40.0
     argBox.IsReadOnly <- false
     argBox.AcceptsReturn <- true
     argBox.MaxLines <- 1
     let mutable filePan = new StackPanel()
     filePan.Orientation <- Orientation.Horizontal
     let _ = filePan.Children.Add(fileBox)
     let _ = filePan.Children.Add(argBox)
     let fAMenu = new Menu()
     let fAFep = new MenuItem()
     fAFep.Header <- filePan
     fAFep.Tag <- item.Tag
     fAFep.Click.Add(eXecReq)
     let _ = fAMenu.Items.Add(fAFep)
     filePopup.Child <- fAMenu
     filePopup.IsOpen <- true
     let _ = Keyboard.Focus argBox
     filePopup.StaysOpen <- false
    with
     |e -> eprintf "\n\n ERROR: %O\n" e
  elif item.HasItems = false then 
   if e.Key = Key.Enter then
    if File.Exists(item.Tag.ToString()) then
     let itemToOpen = item.Tag.ToString()
     let info:ProcessStartInfo = new ProcessStartInfo(itemToOpen)
     let rc =  Process.Start(info)
     let _ = Keyboard.Focus item
     textBox1.Text <- "Opened " + itemToOpen

    else
     item.Items.Clear()
     item <- item.Parent:?>TreeViewItem 
     item.Items.RemoveAt(0)
     //let mutable fi:FileInfo = null
     let mutable insInd = 0
     let info:DirectoryInfo = DirectoryInfo(item.Tag.ToString())
     for f:FileInfo in info.GetFiles() do
      let lSubitem = new TreeViewItem ( )
      lSubitem.Header <- f.Name
      lSubitem.Tag <- f.FullName
      lSubitem.FontWeight <- FontWeights.ExtraBold
      lSubitem.ToolTip <- "Size:  "+f.Length.ToString()+
      "   Index:  "+insInd.ToString()+ 
      "   Attr:   "+f.Attributes.ToString()+ 
      "\nDate Modified:  "+f.LastWriteTime.ToString()+
      "\nDate Accessed:  "+f.LastAccessTime.ToString()+
      "\nDate Created :  "+f.CreationTime.ToString()
      item.Items.Insert(insInd, lSubitem )
      insInd <- insInd + 1 
      done
     Keyboard.Focus item|>ignore
     myScrollViewer.PageLeft()
                   
    textBox1.Text <-  item.Tag.ToString()
   textBox1.Text <-  item.Tag.ToString()
  else 
   if  e.Key = Key.Enter then
    if item.IsExpanded then
     item.IsExpanded <- false
    item.Items.Clear()
    let _ = item.Items.Add ( dummyNode )
    item.IsExpanded <- true
    textBox1.Text <-  item.Tag.ToString()

  mainWindow.Title <- item.Tag.ToString()
  focusItem <- item
 with
  |e -> eprintf "\n\n ERROR: %O\n"

This eventhandler increses and decreases the textbox font when you hold down the control key and rotate the mouse wheel.

F#
let textBox_MouseWheel(e:MouseWheelEventArgs) =
    if Keyboard.IsKeyDown(Key.RightCtrl) || Keyboard.IsKeyDown(Key.LeftCtrl) then
       if e.Delta > 0 then textBox1.FontSize <-  textBox1.FontSize + 2.0
       if e.Delta < 0 then 
          if textBox1.FontSize > 12.0 then textBox1.FontSize <-  textBox1.FontSize - 2.0

This eventhandler handles font size and scrolling for the treeTrunk. WinForms handles both vertical and horizontal scrolling, but not WPF. Therefore, if you need to scroll horizontally, you have to handle it yourself. The ScrollViewer does support it but doesn't automatically provide it. At least, I couldn't find it. I have provided 2 ways to scroll horizontally. The first is to hold down the shift key and rotate the wheel. The second method detects when the delta property is equal to zero. This means the wheel has been tilted. I know of no way to determine which way it tilted so I can not scroll left or right but I do know the user wants to scroll horizontally so I turn off the vertical scroll bar and turn on the horizontal scroll bar. Now the user can rotate the mouse wheel, UP becomes RIGHT, DOWN becomes LEFT. You can reverse this if you wish. If you can determine the direction of tilt then you can use the tilt to drive horizontal scrolling. You could use the F# Interoperability but I am not doing that now.

Now to scroll vertically the user tilts the wheel and the eventhandler turns off the horizontal scroll bar and turns on the vertical scroll bar. This way the user que and the function's que are the same. You can't change one and forget to change the other, since they are the same.

F#
let folders_MouseWheel(e:MouseWheelEventArgs) =
    if Keyboard.IsKeyDown(Key.RightShift) || Keyboard.IsKeyDown(Key.LeftShift) then
       if e.Delta < 0 then myScrollViewer.LineLeft()
       if e.Delta > 0 then myScrollViewer.LineRight()
    elif Keyboard.IsKeyDown(Key.RightCtrl) || Keyboard.IsKeyDown(Key.LeftCtrl) then
       if e.Delta > 0 then treeTrunk.FontSize <-  treeTrunk.FontSize + 2.0
       if e.Delta < 0 then 
          if treeTrunk.FontSize > 12.0 then treeTrunk.FontSize <-  treeTrunk.FontSize - 2.0
    elif myScrollViewer.VerticalScrollBarVisibility = ScrollBarVisibility.Visible then
       if e.Delta < 0 then myScrollViewer.LineDown()
       if e.Delta > 0 then myScrollViewer.LineUp()
       if e.Delta = 0 then 
          myScrollViewer.VerticalScrollBarVisibility <- ScrollBarVisibility.Hidden
          myScrollViewer.HorizontalScrollBarVisibility <- ScrollBarVisibility.Visible
    elif myScrollViewer.VerticalScrollBarVisibility = ScrollBarVisibility.Hidden then
       if e.Delta < 0 then myScrollViewer.LineLeft()
       if e.Delta > 0 then myScrollViewer.LineRight()
       if e.Delta = 0 then 
          myScrollViewer.VerticalScrollBarVisibility <- ScrollBarVisibility.Visible
          myScrollViewer.HorizontalScrollBarVisibility <- ScrollBarVisibility.Hidden

The next routine handles the textinput and calls the recursive routine to advance the cursor to the next item that begins with the typed character.

F#
let textInput(e:TextCompositionEventArgs)=
    if (e.Source :? TreeViewItem) then
       let mutable item = new TreeViewItem()
       item <- e.Source:?>TreeViewItem
       let mutable eKey = new String(null)
       eKey <- e.Text
       let mutable whatFound:string =  "nothing"
       let mutable pItem = new TreeViewItem()
       for i in 0 .. (treeTrunk.Items.Count) - 1 do
          pItem <- treeTrunk.Items.[i]:?>TreeViewItem
          if pItem.IsExpanded then
             whatFound <- findThisChar(pItem, item, eKey, whatFound)

This next bit is the event handler that populates the 'nodes' when they have their 'IsExpanded' property set to 'true'. This algorithm builds a string containing the files in this Directory, separated by spaces. It is placed before the sub-folders so that it is adjacent to it's parent folder. The key-up eventhandler contains the code to 'expand' the string into a 'list of nodes', allowing the selection and processing of individual files.

It also sets the 'IsExpanded' property for any folder that is contained in the user defined Starting Directory. Whe the 2 values are equal, 'focusItem', the 'item' that is to receive Focus when the Window is shown is set to the current item, 'tSubitem'.

F#
let folder_Expanded(e:RoutedEventArgs)=
 myScrollViewer.PageLeft()
 let mutable item = new TreeViewItem()
 item.Focusable <- true
 item <- e.Source:?>TreeViewItem
 if (item.Items.Count = 1 && item.Items.[0] = dummyNode) then do
  item.Items.Clear()
  try
   let sb = new StringBuilder(32)
   for s in Directory.GetFiles ( item.Tag.ToString ( ) ) do
    sb.Append(" "+s.Substring ( s.LastIndexOf ( "\\" ) + 1 ))|>ignore
    done
   if (sb.ToString() <> "") then do
    let mutable fs = sb.ToString()
    fs <- fs.Substring (1)
    let lSubitem = new TreeViewItem ( )
    lSubitem.Header <- fs
    lSubitem.Tag <- item.Tag
    lSubitem.FontWeight <- FontWeights.ExtraBold
    item.Items.Add ( lSubitem )|>ignore
       
   for s in Directory.GetDirectories ( item.Tag.ToString ( ) ) do
    let tSubitem = new TreeViewItem ( )
    tSubitem.Header <- s.Substring(s.LastIndexOf("\\") + 1)
    tSubitem.Tag <- s
    tSubitem.FontWeight <- FontWeights.ExtraBold
    let _ = tSubitem.Items.Add ( dummyNode )
    item.Items.Add ( tSubitem )|>ignore
    if startInDir.Contains(tSubitem.Tag.ToString()) then 
     if startInDir <> tSubitem.Tag.ToString() then
      tSubitem.IsExpanded <- true
     if startInDir = tSubitem.Tag.ToString() then
      focusItem <- tSubitem
    done
  with
   |e -> eprintf "\n\n ERROR: %O\n" e

The line below is a 'lambda' or unnamed function to handle the 'FrameWorkElement.Loaded' event which is fired when the element (mainWindow) is laid out, rendered, and ready for interaction. The 'fun(ction)' will first attempt to read 'Isolated Storage' for this assembly, based on the assemblies strong name, to get the user defined 'Starting Directory'. If this fails, an error message is printed to standard error and the programmed value is used. You can change this by editting the 'let' statement that defines 'startInDir'. The 'IsolatedStorageFile', 'ifl' is defined at the same program location because both stream reader and stream writer must both use the same 'object'.

As the comment says, if you (the program) got there, you have read the starting directory and the cursor should be positioned on it.

F#
mainWindow.Loaded.Add(fun _ ->
  try
    let mutable isf:IsolatedStorageFileStream = 
	new IsolatedStorageFileStream("startInDir.ips", FileMode.Open, ifl)
    let mutable sr:StreamReader = new StreamReader(isf)
    startInDir <- sr.ReadToEnd()
    sr.Close() // if you got here then you have read
    // the starting directory you saved previously
  with
    |e -> eprintf "\n\n ERROR: %O\n" e

The next line gets the collection of 'drives' on the system. This is followed by an error handler, (try..with), for the 'for ? in ??' loop it encapulates. The loop defines the 'node', 'let item = ...' and populates it with the drive information. In this loop a routed event call is added to the item's events collection. A 'dummyNode' is added so the 'node' can be 'Expanded'. This combination eliminates the need to recursively populate the tree.

Note that if the Starting Directory 'Contains' the items's 'Tag' the item is 'expanded'. Next the item is brought into view and selected. With that We are 'done' with the loop but not the function.

F#
let drives = Directory.GetLogicalDrives()
  try
    for d in drives do
      let item = new TreeViewItem()
      item.Header <- d 
      item.Tag <- d
      item.FontWeight <- FontWeights.Normal
      let _ = item.Items.Add(dummyNode)
      item.Expanded.Add(folder_Expanded)
      let _ = treeTrunk.Items.Add(item)
      if startInDir.Contains(item.Tag.ToString()) then 
         item.IsExpanded <- true
      item.BringIntoView()
      item.IsSelected <- true
      done
  with
    |e -> eprintf "\n\n ERROR: %O\n" e

This last block of the 'Loaded' function adds the eventhandlers for the user interactions. Note the MouseRightButton events. The

PreviewMouseRightButtonDown 
could be written with or without the preview prefix but it is recommended that the prefix be used. The other button however,
MouseRightButtonUp 
event can not use the preview prefix in combination with PreviewMouseRightButtonDown event. If you want to use only the context menu then comment the 'menuReq' line. These are the 2 lines you would comment if you pasted these code segments, without the menu definitions, into F# INTERACTIVE (don't forget the ';;').

Finally We finish the 'Loaded' function by bringing the focusItem into view and selecing it. The Right Parenthesis marks the end of the function.

Now the only thing left to do is put the Grid into the Window, show the Window, and it's Title and give Keyboard Focus to 'focusItem'.

F#
  textBox1.PreviewMouseWheel.Add(textBox_MouseWheel)
  treeTrunk.PreviewMouseLeftButtonDown.Add(focusReq)
  treeTrunk.PreviewKeyUp.Add(keyUpDetected)
  treeTrunk.PreviewTextInput.Add(textInput)
  //treeTrunk.PreviewMouseRightButtonDown.Add(menuReq)
  //treeTrunk.MouseRightButtonUp.Add(contextReq)
  myComputer.PreviewMouseWheel.Add(folders_MouseWheel)
  treeTrunk.IsEnabled <- true
  focusItem.IsExpanded <- true
  focusItem.BringIntoView()
  focusItem.IsSelected <- true
  ) 
   
mainWindow.Content <- myGrid // Put the Grid in the Window
mainWindow.Show ()
mainWindow.Title <- "the Explorer Imperative"
focusItem.Focus() // Give the Keyboard Focus to focusItem

// FSI.EXE will ignore the rest of the program but
// FSC.EXE will Compile it.
#if COMPILED
[<STAThread>]
[<EntryPoint>]
let main(_) = (new Application()).Run(mainWindow)
#endif

There is a great deal of functionality in this program that I have not yet explained but I feel the need to write another article and it is imperative that I do not try to put it all in this one.

Background

I often find it necessary, for various reasons, to use a screen resolution that displays fonts in a size that is very hard for me to read. I have long intended to write a utility to augment Windows Explorer with a zoom feature, similar to IE. I wrote this for personal use but found that just a zoom feature was usually not enought incentive to execute it Then I added some other functions I would like to see in Windows Explorer, along with the ability to send a folder to Explorer when I wanted to do something I had not yet put into my program.

Using the Code

The individual code seqments use .NET Objects and routed events that are fired from a treviewitem or a menu. There aremany applications that use tree structures and menues. Many of them can use some of these functions. Just add the event handler call to the object that wants to initiate the function.

The program executes the same whether you compile it it or run it unteractively. The only real difference is you have to create a project to compile it, unless you can do the command line compile. There are some real advantages to having a project, But it's like having a frying pan and a griddle(you ca fry your eggs and make your pancakes at the same time). With a project you can add something by just clicking and the IDE takes care of it, without the project you have to do it manually.

The 'Explorer Imperative' was written as a single file WPF program using code-only, no XAML. Idecided to use text input rather than raw input but that required the inclusion of the WPF Event Loop. This code is the copyrighted property of Micro Soft. It is only needed for the script version of the program. The compiled program does not require it. If you create a project and compile the program, you should not downkoad the IwshRuntimeLibrarybut should add a com reference to The Windows Script Host Object Model, this creates a wrapper for the com object that allows it to be used like a .Net Object. If the wrapper doesn't match the com object or if the com object is not present, the 'Create a Short Cut' functionality will not be available. In this case, delete the munuitem and the event handler for it.

History

  • 13th July, 2011: Initial version

License

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