Explorer Imperative - 第三部分
从 Continuation 恢复 UI 响应能力(异步文件 IO)。
屏幕截图,显示搜索菜单、进度条和当前在后台线程中处理的目录(在文本框中显示)。
寻找响应式 UI
在之前的 Explorer Imperative 中,我添加了一个目录搜索功能,并为其启用了菜单选择。我测试过,在我的 XP 上看起来很酷。在我的 XP 上运行得非常快。以我短浅的视角来看,我认为每个人都会有和我一样棒的用户体验。然后我得到了一台运行着现代操作系统的现代计算机(Windows 7),当我有时间的时候,我下载了最新版的 F# 和 .NET,并启动了我那个小玩具程序。在 Windows 7 上它看起来更好,直到我进行搜索,然后,我的天哪,它卡住了!我想,我得修好它。我无法移动窗口来查看下面的控制台输出,当我切换到控制台时,主窗口消失了。我无法切换回它。
但是,我被其他事情打断了,当我回来时,窗口又出现了。我刚刚体验到了一个由长时间运行的 I/O 进程阻塞 GUI 线程导致的无响应 UI。对于任何因这次经历而降低对 F# 看法的人,我致以诚挚的歉意。如果你们给我机会,我将努力提升你们对 F# 及其功能的看法。但我必须偏离严格的命令式范式,引入一些更强大、因此可能不那么容易理解的函数式结构,这些结构赋予了 F# 强大的功能和多功能性。
这篇文章是一个(我的)挽回颜面的方式,我将尝试解释如何使用类型扩展为阻塞 GUI 线程的长时 I/O 例程添加异步处理。类型扩展的代码是微软公司的版权财产,可以在 Microsoft 帮助库的“Async.FromContinuations<'T>
方法 (F#)”标题下找到。您必须参考该文档,作为对 BackgroundWorker
类的官方和正确描述。我的评论应被视为对我为何以这种方式构建使用它的代码的解释。这并非声称是最好或唯一的方法,而只是集成 BackgroundWorker
到程序中的一种可能方式。F# 类型定义是对‘System.ComponentModel.BackgroundWorker
’中的 BackgroundWorker
类的扩展。‘with
’关键字将此类型定义标识为‘BackgroundWorker’类型的‘扩展’。此‘扩展’创建了一个异步计算,在 RunWorkerCompletedEventHandler
中封装了‘cont’(成功续接)、‘econt’(异常续接)和‘ccont’(取消续接)。回调最终会调用其中之一。我对这个过程的解读是,‘handler’将根据‘RunWorkerCompletedEventHandler
’事件的‘args’,在‘RunWorkerCompleted’(完成,即发生 RunWorkerCompleted 事件)时,从三个‘Continuations’中选择‘From’。当‘AsyncRunWorker
’正在计算你的‘computation’时,你的‘worker’例程‘ReportsProgress
’,以及跟踪错误和取消请求。
这是类型扩展的代码
type BackgroundWorker with
member this.AsyncRunWorker (computation, argument : 'T, progressChangedHandler) : Async<'U> =
let workerAsync =
Async.FromContinuations (fun (cont, econt, ccont) ->
let handler = new RunWorkerCompletedEventHandler (fun sender args ->
if args.Cancelled then
ccont (new OperationCanceledException())
elif args.Error <> null then
econt args.Error
else
cont (args.Result :?> 'U))
this.WorkerSupportsCancellation <- true
this.WorkerReportsProgress <- true
this.DoWork.AddHandler(new DoWorkEventHandler(fun sender args ->
args.Result <- computation(argument, this, args)))
this.ProgressChanged.AddHandler(progressChangedHandler)
this.RunWorkerCompleted.AddHandler(handler)
this.RunWorkerAsync(argument)
)
async {
use! holder = Async.OnCancel(fun _ -> this.CancelAsync())
return! workerAsync
}
简而言之,这种类型扩展通过在后台执行来恢复 GUI 的响应能力,允许任务被取消,并且还允许异常处理和进度报告到 GUI 线程。为了利用这些可能性,我在搜索菜单中添加了一个“取消搜索”选项,并在文本框上方添加了一个进度条。搜索状态输出显示在文本框中。
背景
所有主要的 IDE 都内置了出色的搜索功能,通常还带有替换功能。Windows 7 本身就内置了强大的搜索功能。你可以从开始菜单或 Windows 资源管理器中使用它。它太棒了!
但有时它并不完全符合我的要求。有时我几乎想要 grep!不幸的是(或者也许是幸运的是),它发生的频率不够高,以至于我记不住所有那些开关以及它们的作用,甚至记不住我把那本书放在哪里了。通常我想要看到的是某个字符串或关键字出现的行,但我也想看到它之前的一行和之后的一行。而且,虽然我可能想浏览整个文件,但又不想在我的编辑器或 IDE 中打开它,因为它会改变“最近”文件或项目的列表。很快,列表里就什么我实际在处理的东西都没有了。所以我在记事本中打开它们,并使用它的查找功能。通常控制台窗口中的三行“上下文”输出会显示我想要的东西。之前,我将文件搜索和“上下文”输出功能添加到了 Explorer Imperative 中,我打算在有时间的时候为文本框添加一个“查找”功能。然后我发现,在 Windows 7 机器上,文件搜索会阻塞 UI。因此,在这一集中,我将同时解决这两个问题。
后台工作者
上面显示的“类型扩展”提供了返回类型为 Async<'U>(通用 Async 类型)的成员 'this.AsyncRunWorker',其参数为 'computation'(你必须提供)、'argument'(通用类型,'T)和 'progressChangedHandler'。‘argument’类型必须与定义‘computation’的 let 绑定中命名的参数类型相同。在这种情况下,它是“let parseDirsAsync (path, worker: BackgroundWorker, eventArgs: DoWorkEventArgs) = ”。为了理清联系,‘parseDirsAsync’是‘computation’,‘path’是‘argument’,‘worker’是调用 BackgroundWorker 例程的代码中定义的 lambda 表达式。这是 computation 的代码。
let parseDirsAsync (path, worker: BackgroundWorker, eventArgs: DoWorkEventArgs) =
// Define the computation
let mutable root = ""
if File.Exists(path) then do // it's a file.extract the directory and fname
fname <- path.Substring(path.LastIndexOf("\\") + 1)
root <- path.Substring(0,path.LastIndexOf("\\"))
else // it's a directory. use it and current fname
root <- path
printfn "Searching for %s in %s and it's subdirectories." fname root
// create 3 empty string arrays on the heap
let fileList:string[] ref = ref (Array.create 1 "")
let files:string[] ref = ref (Array.create 1 "")
let subDirs:string[] ref = ref (Array.create 1 "")
let gotIt = ref false //create a bool on the heap
let rec fileSearch root = //Define the recursive function
gotIt := false //':=' operator puts a value in the ref cell
try
files := Directory.GetFiles(root, fname, SearchOption.AllDirectories)
gotIt := true // only happens when getfiles succeeds
with
| :? System.UnauthorizedAccessException -> ()
| _ as oops -> invalidOp <| sprintf "%O" oops
if (worker.CancellationPending) then
eventArgs.Cancel <- true
elif !gotIt = true then do // '!' operator dereferebces a ref cell
let mutable count = 0
fileList := Array.append !fileList !files // build the fileList
let len = files.Value.Length //number of array elements
if len > 0 then printfn "\nIn %s" root
for fi in !files do
printfn " found %s" fi
count <- count + 1 // builds from 0 to len
let percentComplete = int ( ((float count) / (float (len))) * 100.0)
worker.ReportProgress(percentComplete, fi)
else do
gotIt := false //make sure it's false now
try
files := Directory.GetFiles(root, fname) //, SearchOption.TopDirectoryOnly)
gotIt := true
with
| :? System.UnauthorizedAccessException -> ()
| _ as e -> invalidOp <| sprintf "%O" e
if (worker.CancellationPending) then
eventArgs.Cancel <- true
elif !gotIt = true then do
let mutable count = 0
fileList := Array.append !fileList !files //|>ignore
let len = files.Value.Length
if len > 0 then printfn "\nIn %s" root
for fi in !files do
printfn " found %s" fi
count <- count + 1
let percentComplete = int ( ((float count) / (float (len))) * 100.0)
worker.ReportProgress(percentComplete, fi)
try
subDirs := Directory.GetDirectories(root) //, "*", SearchOption.TopDirectoryOnly)
gotIt := true
with
| :? System.UnauthorizedAccessException -> ()
| e -> invalidOp <| sprintf "%O" e
if (worker.CancellationPending) then
eventArgs.Cancel <- true
else do
if !gotIt = true then do
let mutable count = 0
let len = subDirs.Value.Length
for di in !subDirs do
count <- count + 1
let percentComplete = int ( ((float count) / (float (len))) * 100.0)
worker.ReportProgress(percentComplete, di)
fileSearch di
fileSearch root //Execute the file search function
!fileList //return fileList as the result
为了理清例程的意图,我们将递归地获取 fname 中匹配模式的所有文件。如果我们能做到,我们就从所有目录中获取文件。如果成功,我们就完成了,否则我们就从顶层目录获取文件,然后用顶层目录的所有子目录进行递归。由于 fileSearch 表达式是一个闭包,我们无法在其内部捕获可变值。我们必须在堆上传递所有内容。这就是为什么我们必须使用 'ref'、'!' 和 ':='。由于我们将 'fileSearch' 定义在另一个函数内,我们必须在该函数内调用它。然后我们返回我们构建的文件列表,'!fileList'。
调用 BackgroundWorker
下面的简短片段定义了表达式 'computation' 及其所有部分。为了将片段与定义 parseDirsAsync 的 let 绑定关联起来,'value' 是 'path' 参数。lambda 表达式 '(fun sender eventArgs -> ...)' 是参数列表中的 'worker'。实际上,值 'parseDirsAsync' 是传递给 worker 的一个函数,而 worker 是 parseDirsAsync 的一个参数。F# 编译器理解这一点,但我还在思考它。
第二行和第三行安全地更新了 UI,报告了当前正在处理的目录以及该目录完成的百分比。由于我们实际上不知道将要处理的文件或目录的总数,我们任意选择报告我们正在处理的目录的进度,而不是整个任务。
第四行 'Async.StartWithContinuations(...)' 在后台启动计算,并将成功的结果馈送到第一个续接,异常的结果馈送到第二个续接,取消的结果馈送到第三个续接。请注意,此片段前面有一些初始化代码,后面是一些在 GUI 线程上完成处理的代码,换句话说,就是三个续接。
let computation value = worker.AsyncRunWorker(parseDirsAsync, value, (fun sender eventArgs ->
textBox1.Text <- "Scanning ... " + eventArgs.UserState.ToString()
myProgressBar.Value <- (float (eventArgs.ProgressPercentage)) ))
Async.StartWithContinuations( computation value, (fun result ->
此片段之后的行包含成功续接的主体,它以一个闭括号、一个逗号和一个开括号以及关键字 'fun' 结束,这是异常续接的 lambda 表达式的开始。此 lambda 以相同的模式结束,左括号、逗号、右括号,然后是关键字 fun,开始处理取消的第三个续接。取消续接以一个闭括号结束,然后是另一个闭括号,它终止了 Async.StartWithContinuations lambda 表达式本身。在成功续接中,我添加了将 fileList 入队并将列表中嵌入的任何空字符串剥离的代码,方法是附加一个空字符串到数组。然后,如果当前项在队列中,我将队列定位到下一个可用项,以便找到下一个匹配项。如果当前项不在队列中,我们就定位到队列的开头。我们找到该项并尝试将其定位在视口的中间。这在一个名为 'updateMyScreen' 的函数中完成。其他两个续接在文本框中放入一条消息,指示发生了错误或取消,并将 'fileSearchStarted' 标志设置为 'false',以便下一个构建的搜索菜单可以启用所有异步搜索点击并禁用取消点击。当满足某些条件时,不会执行异步文件搜索。我们唯一需要做的就是调用 findNextFile() 和 updateMyScreen() 例程。
这是异步文件搜索的菜单请求处理程序
let findFileReq (myProgressBar:ProgressBar) (textBox1:TextBox) value =
try
let mutable currentDir = ""
let mutable currentfname = ""
let temp = focusItem.Tag.ToString()
if File.Exists(temp) then do
currentDir <- temp.Substring(0,temp.LastIndexOf("\\"))
if fname = "???" || fname = "" then do
currentfname <- temp.Substring(temp.LastIndexOf("\\") + 1)
else
currentDir <- temp
if fname <> "" then
fSfname <- fname
elif currentfname <> "" then
fname <- currentfname
fSfname <-currentfname
else
fSfname <- "???"
//endif
if (fQue.Count = 0) || (fSfname <> fTfname) then
fname <- fSfname
let targetFile = currentDir + "\\" + fname
printfn "\nCurrent File is: %s." targetFile
fQue.Clear()
mainWindow.Title <- "Searching for " + fname
let worker = new BackgroundWorker()
fQue.Clear()
fileSearchStarted <- true
textBox1.Text <- "Computing..."
let computation value = worker.AsyncRunWorker(parseDirsAsync, value, (fun sender eventArgs ->
textBox1.Text <- "Scanning ... " + eventArgs.UserState.ToString()
myProgressBar.Value <- (float (eventArgs.ProgressPercentage)) ))
Async.StartWithContinuations( computation value, (fun result ->
printfn "\nThe following files have been Enquwued:"
Array.iter (fun elem -> (printfn "%s" elem;fQue.Enqueue elem)) result
fileSearchStarted <- false
startInDir <- fQue.Dequeue()
while startInDir = "" && fQue.Count <> 0 do
startInDir <- fQue.Dequeue()
fQue.Enqueue(startInDir)
if fQue.Contains(targetFile) then
while startInDir <> targetFile do
startInDir <- fQue.Dequeue()
fQue.Enqueue(startInDir)
let mutable qItem = new TreeViewItem()
let mutable pItem = new TreeViewItem()
for i in 0 .. (treeTrunk.Items.Count) - 1 do
pItem <- treeTrunk.Items.[i]:?>TreeViewItem
if startInDir.Contains(pItem.Tag.ToString()) then
findIt pItem
textBox1.Text <- (focusItem.Tag.ToString())
done
mainWindow.Title <- focusItem.Tag.ToString()
myProgressBar.Value <- 0.0
findNextFile()
printfn "Current File is:\n%s" (startInDir)
updateMyScreen()
// the end of the success continuation
),
(fun exn ->
textBox1.Text <- "Operation failed with error:" + exn.Message
fileSearchStarted <- false
// the end of the exception continuation
),
(fun _ ->
textBox1.Text <- "Operation canceled."
fileSearchStarted <- false
// the end of the cancellation continuation
) // the end of StartWithContinuations
else
findNextFile()
printfn "Current File is:%s\n" (startInDir)
updateMyScreen()
with
|e -> eprintf "Error: "
'startHereReq' 请求调用 'findFileReq',但传递给它的是当前目录而不是驱动器的默认根目录。这使用户能够搜索任何驱动器上的任何位置。'findString' 例程本身与 Explorer Imperative 的第二部分相同,但嵌入在 'findStringReq' 中的文件搜索已更改为后台版本。字符串搜索本身不是异步的,但速度足够快,可以避免长时间阻塞 GUI。我认识到有些人可能不同意,但我不想争论,我邀请他们使用这种类型扩展或任何其他他们想要使用的技术来编写一个新的后台字符串搜索。
菜单。启动异步文件搜索
屏幕截图,显示搜索菜单,其中包含从屏幕获取的文件名,并邀请“打我!”来搜索“???”。
为了防止用户意外地在搜索进行时启动搜索,有必要在搜索开始时禁用选项。我们还需要保持弹出菜单打开,以便用户可以根据需要取消搜索。但是,当搜索完成时,我们需要关闭弹出菜单,以便构建一个新的菜单,并重新启用选项。这意味着我们必须关闭弹出菜单。因此,弹出菜单必须在模块级别定义。这允许我们在需要时关闭它。请注意,只有弹出菜单在模块级别创建。它被命名为 'menuPopup'。菜单在菜单构建例程的末尾添加到弹出菜单中。我们将在 'updateMyScreen()' 函数中关闭弹出菜单。通过将其 'IsOpen' 标志设置为 false 来实现。这是代码。
menuPopup.IsOpen <- false
我们还必须设置一个标志来指示搜索已完成。它在模块级别被命名为 'fileSearchStarted',并在异步调用的成功、异常和取消续接中设置为 false。它在调用之前被设置为 true。它在搜索菜单项的菜单构建过程中进行测试。这是必要的,因为用户可以通过在打开的菜单外部点击来强制关闭菜单。发生这种情况时,如果用户认为有必要取消搜索,则菜单将仅以“取消”选项启用来构建。本文开头的屏幕截图显示了一个正在进行搜索的菜单。请注意,除“取消”选项外,所有选项都已灰显。另请注意,“查找下一个文件”和“查找下一个…字符串”选项(括号中为“F3”和“F4”)。按下“F3”或“F4”键将执行与点击其中一个选项相同的操作。这些函数将循环遍历队列。通过“F4”键调用的函数已更新为在文本框中查找字符串。已添加事件处理程序,以便当光标悬停在文本框上时高亮显示选定的单词。我最初“添加”了一个例程和它的触发器,但当我看到例程有多短时,我将其更改为 lambda 表达式。我将原始代码保留在程序中。这是一个 lambda 的代码。
textBox1.MouseEnter.Add(fun _ ->
textBox1.Focus()|>ignore
)
每当鼠标指针悬停在文本框上时,此表达式都会触发。它所做的只是切换焦点到文本框并丢弃类型信息。另一个 lambda 就在它下面。当鼠标离开时,它只是将焦点切换到 TreeView。它们都位于 mainWindow.Loaded lambda 表达式的末尾。这并不是菜单的真正一部分,但在这里很重要,因为“F3”键将查找文本框中字符串的下一个出现。如果您正在查看 TreeView 并且鼠标指针恰好悬停在文本框上,您可能会错过正在发生的事情。
现在回到菜单。我在第二部分讨论了菜单,所以我只介绍新代码。我们已经创建了 menuPopup Popup,因此当菜单被添加到其中时,不要感到惊讶。这是搜索菜单的代码。它以代码片段的形式出现,以缩短文章。
let menuReq(e:MouseButtonEventArgs) =
try
//let menuPopup = new Popup() - now created at module level, Line 291
.....
let pmS = new MenuItem()
pmS.Header <- "Search"
let pmSfF = new MenuItem() // We create
let pmSfFsH = new MenuItem() // all of the
let pmSfS = new MenuItem() // MenuItems
let pmSfFsN = new MenuItem() // in order to
let pmSfFsS = new MenuItem() // enable or
let pmSasynCan = new MenuItem()// disable them enmass
if fileSearchStarted = true then
pmSfF.IsEnabled <- false
pmSfFsH.IsEnabled <- false
pmSfS.IsEnabled <- false
pmSfFsN.IsEnabled <- false
pmSfFsS.IsEnabled <- false
pmSasynCan.IsEnabled <- true
else
pmSfF.IsEnabled <- true
pmSfFsH.IsEnabled <- true
pmSfS.IsEnabled <- true
pmSfFsN.IsEnabled <- true
pmSfFsS.IsEnabled <- true
pmSasynCan.IsEnabled <- false
let pmSfFHeader = new TextBox()
pmSfFHeader.Text <- "Search For File"
...
pmSfF.Tag <- thisItem.Tag.ToString()
pmSfF.StaysOpenOnClick <- true
pmSfF.Click.Add(fun args ->
findFileReq myProgressBar textBox1 (Directory.GetDirectoryRoot(thisItem.Tag.ToString()))
pmSfF.IsEnabled <- false // In the line above we are passing the progressBar,
pmSfFsH.IsEnabled <- false // textBox1 and the root of the directory.
pmSfS.IsEnabled <- false
pmSfFsN.IsEnabled <- false
pmSfFsS.IsEnabled <- false
pmSasynCan.IsEnabled <- true
)
pmSfF.Header <- pmSfFs
let pmSfFToolTip = new ToolTip()
pmSfFToolTip.FontSize <- sizeOfFont * 2.0
pmSfFToolTip.FontWeight <- FontWeights.ExtraBold
pmSfFToolTip.Content <- "HIT ME ! (To Start File Search)"
pmSfF.ToolTip <- pmSfFToolTip
let _ = pmS.Items.Add(pmSfF)
pmSfFsH.Header <- "Start File Search Here"
pmSfFsH.Tag <- thisItem.Tag.ToString()
pmSfFsH.StaysOpenOnClick <- true
pmSfFsH.Click.Add(fun args ->
fTfname <- "..." // Here we set the values to force an asynchronous file search
fSfname <- "???" // Note below we are passing the progressBar, textBox1 and the current directory
findFileReq myProgressBar textBox1 (thisItem.Tag.ToString())
pmSfFsH.IsEnabled <- false
pmSfF.IsEnabled <- false
pmSfS.IsEnabled <- false
pmSfFsN.IsEnabled <- false
pmSfFsS.IsEnabled <- false
pmSasynCan.IsEnabled <- true
)
let pmSfFsHToolTip = new ToolTip()
pmSfFsHToolTip.FontSize <- sizeOfFont * 2.0
pmSfFsHToolTip.FontWeight <- FontWeights.ExtraBold
pmSfFsHToolTip.Content <- "HIT ME ! (To Search this Directory and it's SubDirectories for File)"
pmSfFsH.ToolTip <- pmSfFsHToolTip
let _ = pmS.Items.Add(pmSfFsH)
let cfilePan = new StackPanel()
cfilePan.Orientation <- Orientation.Horizontal
let mutable cargBox = new TextBox()
cargBox.Text <- "Sratch For String"
cargBox.Margin <- new Thickness(10.0,2.0,2.0,2.0)
cargBox.MinWidth <- 15.0
cargBox.IsReadOnly <- true
...
cfileBox.LostFocus.Add(validateFileBox)
cfileBox.IsReadOnly <- false
...
let mutable argBox = new TextBox()
argBox.Margin <- new Thickness(10.0,2.0,2.0,2.0)
argBox.MinWidth <- 40.0
let fndStrToolTip = new ToolTip()
fndStrToolTip.FontSize <- sizeOfFont
fndStrToolTip.FontWeight <- FontWeights.Bold
fndStrToolTip.Content <- "Enter or change the string to search for"
argBox.ToolTip <- fndStrToolTip
if fndStr = "" then
argBox.Text <- "???"
fndStr <- "???"
else
argBox.Text <- fndStr
argBox.LostFocus.Add(validateArgBox)
argBox.IsReadOnly <- false
let mutable inBox = new TextBox()
inBox.Text <- " in "
inBox.Margin <- new Thickness(10.0,2.0,2.0,2.0)
inBox.MinWidth <- 15.0
inBox.IsReadOnly <- true
let _ = cfilePan.Children.Add(cargBox)
let _ = cfilePan.Children.Add(argBox)
let _ = cfilePan.Children.Add(inBox)
let _ = cfilePan.Children.Add(cfileBox)
pmSfS.Header <- cfilePan
pmSfS.Tag <- thisItem.Tag.ToString()
pmSfS.StaysOpenOnClick <- true
pmSfS.Click.Add(fun args ->
findStrReq myProgressBar textBox1 (Directory.GetDirectoryRoot(thisItem.Tag.ToString()))
pmSfS.IsEnabled <- false // Here we are passing the progressBar, the TextBox1
pmSfF.IsEnabled <- false // and the root of the directory
pmSfFsH.IsEnabled <- false
pmSfFsN.IsEnabled <- false
pmSfFsS.IsEnabled <- false
pmSasynCan.IsEnabled <- true
)
let pmSfSToolTip = new ToolTip()
pmSfSToolTip.FontSize <- sizeOfFont * 2.0
pmSfSToolTip.FontWeight <- FontWeights.ExtraBold
pmSfSToolTip.Content <- "HIT ME ! (To Find File with this String in the Queue)"
pmSfS.ToolTip <- pmSfSToolTip
let _ = pmS.Items.Add(pmSfS)
pmSfFsN.Header <- "Find Next File in Queue(F3)"
pmSfFsN.Click.Add(findNextReq) // Here we are passing nothing
let pmSfFsNToolTip = new ToolTip()
pmSfFsNToolTip.FontSize <- sizeOfFont * 2.0
pmSfFsNToolTip.FontWeight <- FontWeights.ExtraBold
pmSfFsNToolTip.Content <- "HIT ME ! (Or HIT PF3 To FIND NEXT File in the Queue)"
pmSfFsN.ToolTip <- pmSfFsNToolTip
let _ = pmS.Items.Add(pmSfFsN)
pmSfFsS.Header <- "Find Next File with this String(F4)"
pmSfFsS.Click.Add(findNextStrReq) // Here we are passing nothing
let pmSfFsSToolTip = new ToolTip()
pmSfFsSToolTip.FontSize <- sizeOfFont * 2.0
pmSfFsSToolTip.FontWeight <- FontWeights.ExtraBold
pmSfFsSToolTip.Content <- "HIT ME ! (Or HIT PF4 To Find Next File with this String in the Queue)"
pmSfFsS.ToolTip <- pmSfFsSToolTip
let _ = pmS.Items.Add(pmSfFsS)
pmSasynCan.Header <- "Cancel Search in Progress"
pmSasynCan.Click.Add(fun args -> Async.CancelDefaultToken() )
let pmSasynCanToolTip = new ToolTip() // The lambda function above cancels any search that might be running
pmSasynCanToolTip.FontSize <- sizeOfFont * 2.0
pmSasynCanToolTip.FontWeight <- FontWeights.ExtraBold
pmSasynCanToolTip.Content <- "HIT ME ! (To Cancel the Asynchronous Search mow in Progress)"
pmSasynCan.ToolTip <- pmSasynCanToolTip
let _ = pmS.Items.Add(pmSasynCan)
let _ = popupMenu.Items.Add(pmS)
.....
文本框查找字符串函数
现在来说点别的,也就是说,完全不一样的东西。文本框的“查找字符串”例程。它没有菜单选项,甚至没有事件处理程序。或者至少,没有专门用于“查找”的事件处理程序。相反,它在 'textBoxKeyUpDetected' 处理程序例程中。目前它只识别 'F3' 功能键,但可以编程为响应任何键。我将分析留给那些有兴趣理解它的人。
文本框查找字符串函数的代码
let textBoxKeyUpDetected(e:KeyEventArgs) =
try
if foundAt = -1 then
foundAt <- textBox1.Text.IndexOf(fndStr, foundAt + 1)
if e.Key.ToString() = "F3" then
foundAt <- textBox1.Text.IndexOf(fndStr, foundAt + 1)
if foundAt = -1 then
foundAt <- textBox1.Text.IndexOf(fndStr, foundAt + 1)
fndStrInd <- textBox1.GetLineIndexFromCharacterIndex(foundAt)
mainWindow.Title <- sprintf "Line #:%d" fndStrInd
textBox1.Select(foundAt, fndStr.Length)
with
|e -> eprintf "\n\n Error: %O\n" e
使用异步文件搜索和 FindString 函数
屏幕截图,显示上下文菜单的备用菜单格式。这会调用与 PopupMenu 格式相同的例程。
在上面的屏幕截图中,显示了上下文菜单。尽管两种格式的选项是在不同的例程中构建的,但它们是相同的。弹出格式在按下鼠标右键时调用。菜单将捕获鼠标指针(如果它在菜单上),否则,当释放鼠标右键时将调用上下文菜单,并且它将捕获鼠标指针。PreviewRightMouseButton<Down|Up> 事件处理程序可以更改为都调用同一个菜单,或者其中一个可以被注释掉。
屏幕截图,显示成功搜索字符串“???”的结果。当鼠标指针悬停在文本框上时,选定的字符串会被高亮显示。
在上面的屏幕截图中,我们已经从“搜索字符串…在文件…中”菜单选项中进行了异步文件搜索。如果鼠标指针悬停在 TreeView 上,找到的文件将被高亮显示。如果鼠标指针悬停在文本框上,选定的字符串(找到的字符串)将被高亮显示。文本框下方的分隔条可以上下移动,以减小或增大文本框的高度。在文本框中,F3 将查找字符串的下一个出现,F4 将不起作用。在 TreeView 中,F3 将转到队列中的下一个文件,F4 将查找队列中包含搜索字符串的下一个文件。
屏幕截图,显示异步文件搜索的控制台输出以及搜索命中时搜索的深度。
在上面的屏幕截图中,我们有显示找到文件的输出以及找到文件时搜索的深度。您可以通过比较以“In…”开头的组标题和“found…”来判断它是否在顶层目录中找到,例如在 MyPdfes 目录中,它在顶层目录中找到一个文件。这些信息可能对您有兴趣,也可能没有。在此信息组之后是入队的文件的列表。
屏幕截图,显示找到字符串参数的行以及之前的行和其后的行,按正确顺序排列。
在上面的屏幕截图中,入队的文件列表后面跟着显示搜索了字符串的文件的消息。此组后面是每个出现的上下文,即前一行、当前行和后一行。使用此输出,您可以快速了解单词在文件中的使用方式,并确定您想要在文本框中更仔细地检查哪个出现。文本框可以扩展到全屏,并且使用查找功能,您可能希望它代替或与记事本结合使用。
屏幕截图,显示鼠标指针悬停在扩展的文本框上,“???”的选定词,“Line #:592”在窗口标题中,以及显示字符串参数找到的行的控制台输出,包括 592。
在上面的屏幕截图中,窗口标题中的“Line #:592”指的是找到选定词的行,而不是视口中可见的第一行。使用此输出,您可以快速了解单词在文件中的使用方式,并确定您想要在文本框中更仔细地检查哪个出现,将控制台输出中的行号与窗口标题中显示的行号进行匹配。文本框可以扩展到全屏,并且使用查找功能,您可能希望它代替或与记事本结合使用。快速提醒 - 在文件菜单上,Open、Execute 和 CMD.EXE 选项可用于“打开”文件。在大多数情况下,“Open”和“Execute”是等效的,因为“打开”文件会执行定义为打开它的程序(即‘.doc’由 Word 打开,‘.pdf’由 Adobe Reader 打开)。这包括“打开”一个文件夹,因为 Windows 资源管理器是“打开”文件夹的程序。“CMD.EXE”菜单项的结果与“Open”和“Execute”相同,但可以接受其他参数。这样做的目的是允许您执行批处理文件,将参数传递给它,或将 TreeView 中的文件名传递给批处理文件。例如,右键单击一个扩展名为 .txt 的文件。在文件菜单中,您可以使用 Open、Execute 或 CMD.EXE 菜单选项“打开”它。另一方面,使用具有 .bat 文件的 Open 菜单选项将“执行”该文件,但不会向其传递任何内容,而使用 Execute 菜单选项则可以。使用 CMD.EXE 菜单选项,您可以例如在第一个输入字段中输入“type”或“notepad”来查看其内容,或者在最后一个输入字段中输入参数并在 Explorer Imperative 中运行它。您还可以使用 NEW 菜单选项就地创建一个不需要参数的批处理文件并“打开”它。只需记住“另存为”并将文件类型更改为“*.*”。最后一点,Open 和 Execute 使用 Comspec 实例,但在完成时会关闭它,而 CMD.EXE 会将其保持打开状态。
历史
历史 第三部分
本系列第三集的原意是为文本框添加一个查找字符串功能。然后我看到搜索大目录树时它会阻塞用户界面多久。我意识到这对用户来说非常糟糕。我找到了一个解决方案,它不仅可以保持 UI 的畅通,还可以为用户提供一点娱乐,让他们保持兴趣,观看进度条和闪过的文字,并且如果您切换到互联网然后再切换回来,窗口就会重新绘制。如果这组代码片段的目的是明确的,那么这将是一个 bug 修复,但除了四处闲逛、探索 F# 并问自己“我能做到这个吗”之外,并没有计划的目的。碰巧网格属于窗口,而树属于网格。然后它就从那里开始。并且,在它产生之后,它就完整地向前发展。换句话说,它自己发明了自己。
这是本文更改的摘要。
- 为 BackgroundWorker 添加了“类型扩展”。这使得长时间运行的函数能够进行异步处理,释放被阻塞的 UI,提供进度条更新和状态信息,并允许后台线程进行取消请求和异常处理。
- 将目录解析例程从递归函数更改为包含递归表达式的函数,该表达式是一个使用外部作用域的可变值的闭包,需要使用 ref 单元格来传递这些值。
- 将创建菜单弹出窗口的代码移至模块级别,以便在菜单例程外部关闭它。
- 更改了搜索菜单及其处理程序,以处理异步处理的调用并防止异步进程的同时执行,从而导致不可预测的结果
- 添加了例程,在鼠标进入文本框时切换焦点到文本框,在鼠标离开时切换回 TreeView。
- 为文本框添加了“查找字符串”功能。通过“F3”调用,它将使用 SelectionBrush 高亮显示“字符串”,并在窗口标题中显示行号。这允许用户匹配控制台窗口上下文输出中的行号。
- 添加了“在队列中查找下一个包含此字符串的文件”。通过“F4”调用,它将循环遍历排队的文件,查找下一个包含当前搜索字符串的文件。
- 添加了例程“updateMyScreen”,以尽可能将具有焦点的 TreeViewItem 移动到屏幕中间。
- 注释掉了处理鼠标滚轮倾斜的代码,因为它会导致滚动仅限于水平滚动,如果鼠标驱动程序没有产生正确的 Delta 值。
- 更改了初始窗口大小,以适应 Windows 7 和 Windows XP 上的物理屏幕。
- 更新了 #I 和 #r 交互指令,仅包括 WPF V4.0 搜索路径并仅引用必要的库。这对于 WPFEventLoop.fsx 也是必需的,以防止 fsi.exe 使用 WPF 事件循环安装中的 WPF V3.0 库,而不是脚本中引用的 WPF V4.0,导致交互执行中出现编译版本中定义的未定义名称。此外,如果未安装 WPF 事件循环,相同的脚本也没有未定义名称。使用当前的 #I 和 #r 指令,脚本可以在 Windows 7 和 XP 上运行,无论它是编译程序还是脚本,都会产生相同的输出。
- 在脚本末尾添加了两个分号,以便粘贴到交互式控制台中。
历史 第二部分
本文致力于讨论 Explorer Imperative 中添加的新功能,特别是 SEARCH 功能,以查找字符串功能为例。讨论了弹出菜单和上下文菜单,因为它们是调用搜索方法所必需的。我将所有菜单代码,包括用于增加和减少菜单字体大小的函数,都视为新代码,即使它存在于原始程序中,因为它当时没有在文章中讨论。虽然菜单本身像现在一样工作,但代码没有显示在文章中。没关系,因为我也没提供对代码的任何解释。因此,除非是 bug 修复或功能上的明显变化,否则我不会列出任何代码更改。
以下是您应该注意的更改;
- “File>New>File”功能在提取菜单项参数的代码中存在一个 bug。在此版本中已纠正此 bug。
- “File>New>Folder”功能在提取菜单项参数的代码中存在一个 bug。在此版本中已纠正此 bug。
- “View>Increase Font Size”功能已更改为增加菜单的大小,而不是树状视图的大小。
- “View>Decrease Font Size”功能已更改为减小菜单的大小,而不是树状视图的大小。
- “Search”菜单已启用,并添加了“查找文件”、“从特定目录开始搜索”、“查找字符串并打印其使用上下文”以及“使用菜单或 F3 键查找下一个文件”的功能。