跳转到内容

Visual Basic/Jarithmetic Round Two 实现

来自维基教科书,开放的书籍,开放的世界

本章介绍了迄今为止讨论内容的实现。

所采用的技术是代码和讨论交织在一起。您应该能够通过简单地复制整个页面并注释掉讨论来提取代码。

之前的讨论一直处于非常高的层次,而实现它将需要高层次和低层次的编码,以及对我们想法的大量改进。

应用程序将由一个窗体、一些模块和一些类组成。我们将从顶部开始,创建应用程序的用户界面,然后我们将逐个添加使它工作的代码。我们将发现,之前讨论中的一些内容是不完整的,有些是误导性的。这在实际开发中很常见。

用户界面

[编辑 | 编辑源代码]

我选择将此程序实现为一个多文档界面应用程序。这通常被称为MDI应用程序。这意味着,可以在程序的同一实例中同时打开多个 Jarithmetic 文档。这是过去大多数 Microsoft Office 应用程序的工作方式。


fMainform.frm

[编辑 | 编辑源代码]

这是一个可能的窗体。图片显示了比第一个版本实际实现的更多菜单,请在您继续进行时实现它们。

MDI form

以下是窗体上控件的声明。您可以将其粘贴到文本编辑器中并将其保存为fMainForm.frm,以便快速开始。

主窗体是用户想要打开的多个文档的容器。每个文档都将是frmDocument实例,请参见下一节。

 VERSION 5.00
 Object = "{F9043C88-F6F2-101A-A3C9-08002B2F49FB}#1.2#0"; "COMDLG32.OCX"
 Begin VB.MDIForm fMainform 
    BackColor       =   &H8000000C&
    Caption         =   "Arithmetic"
    ClientHeight    =   2790
    ClientLeft      =   165
    ClientTop       =   765
    ClientWidth     =   5280
    Icon            =   "Fmainform.frx":0000
    LinkTopic       =   "MDIForm1"
    StartUpPosition =   3  'Windows Default
    Begin MSComDlg.CommonDialog CommonDialog1 
       Left            =   360
       Top             =   240
       _ExtentX        =   847
       _ExtentY        =   847
       _Version        =   393216
    End
    Begin VB.Menu mnuFile 
       Caption         =   "&File"
       Index           =   1
       Begin VB.Menu mnuFileNew 
          Caption         =   "&New"
          Shortcut        =   ^N
       End
       Begin VB.Menu mnuFileOpen 
          Caption         =   "&Open..."
          Shortcut        =   ^O
       End
       Begin VB.Menu mnuFileBar0 
          Caption         =   "-"
       End
       Begin VB.Menu mnuFileSave 
          Caption         =   "&Save"
          Shortcut        =   ^S
       End
       Begin VB.Menu mnuFileSaveAs 
          Caption         =   "Save &As..."
       End
       Begin VB.Menu mnuFileSaveAll 
          Caption         =   "Save A&ll"
       End
       Begin VB.Menu mnuFileClose 
          Caption         =   "&Close"
          Shortcut        =   ^E
       End
       Begin VB.Menu mnuFileCloseAll 
          Caption         =   "&CloseAll"
       End
       Begin VB.Menu mnuFileBar1 
          Caption         =   "-"
       End
       Begin VB.Menu mnuFilePrint 
          Caption         =   "&Print..."
          Shortcut        =   ^P
       End
       Begin VB.Menu mnuFilePrintSetup 
          Caption         =   "&PrintSetup"
       End
       Begin VB.Menu mnuFilePrintPreview 
          Caption         =   "&PrintPreview"
          Shortcut        =   ^R
       End
       Begin VB.Menu mnuFileBar3 
          Caption         =   "-"
       End
       Begin VB.Menu mnuFileSend 
          Caption         =   "&Send"
          Begin VB.Menu mnuFileSendEmail 
             Caption         =   "&Email"
          End
       End
       Begin VB.Menu mnuFileExit 
          Caption         =   "E&xit"
          Shortcut        =   {F4}
       End
    End
    Begin VB.Menu mnuEdit 
       Caption         =   "&Edit"
       Begin VB.Menu mnuEditUndo 
          Caption         =   "&Undo"
          Shortcut        =   ^Z
       End
       Begin VB.Menu mnuEditRedo 
          Caption         =   "&Redo"
       End
       Begin VB.Menu mnueditbar2 
          Caption         =   "-"
       End
       Begin VB.Menu mnuEditCut 
          Caption         =   "Cu&t"
          Shortcut        =   ^X
       End
       Begin VB.Menu mnuEditCopy 
          Caption         =   "&Copy"
          Shortcut        =   ^C
       End
       Begin VB.Menu mnuEditPaste 
          Caption         =   "&Paste"
          Shortcut        =   ^V
       End
       Begin VB.Menu mnueditbar3 
          Caption         =   "-"
       End
       Begin VB.Menu mnuEditSelectAll 
          Caption         =   "&SelectAll"
          Shortcut        =   ^A
       End
    End
    Begin VB.Menu mnuData 
       Caption         =   "&Data"
       Begin VB.Menu mnuEvaluate 
          Caption         =   "&Evaluate"
          Shortcut        =   {F9}
       End
    End
    Begin VB.Menu mnuWindow 
       Caption         =   "&Window"
       WindowList      =   -1  'True
       Begin VB.Menu mnuWindowNewWindow 
          Caption         =   "&New Window"
          Shortcut        =   {F12}
       End
       Begin VB.Menu mnuWindowBar0 
          Caption         =   "-"
       End
       Begin VB.Menu mnuWindowCascade 
          Caption         =   "&Cascade"
       End
       Begin VB.Menu mnuWindowTileHorizontal 
          Caption         =   "Tile &Horizontal"
       End
       Begin VB.Menu mnuWindowTileVertical 
          Caption         =   "Tile &Vertical"
       End
       Begin VB.Menu mnuWindowArrangeIcons 
          Caption         =   "&Arrange Icons"
       End
    End
    Begin VB.Menu mnuHelp 
       Caption         =   "&Help"
       Begin VB.Menu mnuHelpContents 
          Caption         =   "&HelpContents"
          Shortcut        =   {F1}
       End
       Begin VB.Menu mnuHelpTipoftheDay 
          Caption         =   "&TipoftheDay"
       End
       Begin VB.Menu mnuHelpAbout 
          Caption         =   "&About "
       End
       Begin VB.Menu mnuHelpSpecialThanks 
          Caption         =   "&SpecialThanks"
       End
    End
 End
 Attribute VB_Name = "fMainform"
 Attribute VB_GlobalNameSpace = False
 Attribute VB_Creatable = False
 Attribute VB_PredeclaredId = True
 Attribute VB_Exposed = False


现在是窗体的可见代码。它非常短,因为 MDI 窗体没有做太多工作,它主要充当文档窗体的容器。

MDI 窗体有一个文件菜单。当没有打开文档窗体时,它是活动的菜单。当文档窗体打开时,显示的文件菜单属于文档窗体,但我们仍然会调用此方法。当 VB 为我们创建方法时,它将被标记为Private,我们可以将其更改为Public,如这里所做的那样,但将其保留为Private 并添加一个新的Friend 方法来调用它可能会更简洁明了。

当我们打开一个文档时,我们必须首先创建一个文档窗体来保存它。在这个程序实例中,frmDocument 代表了文档。这里做出的设计决策是,每次我们打开一个文档时,它都将加载到 frmDocument 的一个新实例中。这可能是也可能不是合适的,考虑一下如果用户两次打开同一个文档并编辑两个文档会发生什么。

我们使用 Microsoft 提供的通用对话框控件,但我们也可以使用一个包含驱动器、文件夹和文件列表控件的窗体。

 Option Explicit ' always use this to ensure that you don't forget to declare variables
 
 
 Public Sub mnuFileOpen_Click()
     
   Dim oForm As frmDocument
   Set oForm = LoadNewDoc
   
   With CommonDialog1
     ' The title should probably say something meaningful about the application and the document 
     .DialogTitle = "Open" type
     .CancelError = False
     .Filter = gsFILE_FILTER
     .ShowOpen
     If Len(.FileName) = 0 Then
       Exit Sub
     End If
     If Not oForm.LoadFile(.FileName) Then
       MsgBox "Could not load file <" & .FileName & ">," & vbCrLf & "probably couldn't find the zlib.dll.", _
              vbOKOnly + vbCritical, Title
     End If
   End With
 
 End Sub
 

LoadNewDoc 函数与文件打开事件处理程序分离,以便其他调用者可以使用它。

 Public Function LoadNewDoc() As frmDocument
   
   Static lDocumentCount As Long
   lDocumentCount = lDocumentCount + 1
   Set LoadNewDoc = New frmDocument
   LoadNewDoc.Caption = "Document " & lDocumentCount
   LoadNewDoc.Show
 
 End Function
 

当我们卸载主窗体时,我们希望能够确保所有文档都已正确清理,因此我们调用一个函数退出应用程序。我们可以使用返回值来设置Cancel 参数,然后用户可能能够停止关机。

 Private Sub MDIForm_Unload(Cancel As Integer)
   ExitApplication
 End Sub

frmDocument.frm

[编辑 | 编辑源代码]

文档窗体保存了我们的算术文档之一。它并没有比主窗体复杂多少。但是,它确实定义了一些不同的菜单。此外,由于 VB6 的工作方式,它也必须定义所有相同的菜单,它不能从主窗体继承菜单。VB 显示当前窗体定义的菜单,除非只有 MDI 窗体,在这种情况下,将显示 MDI 窗体的菜单。同样,图形显示了比此原型中实际实现的更多菜单。

Document form

以下是菜单和控件的定义

 VERSION 5.00
 Object = "{3B7C8863-D78F-101B-B9B5-04021C009402}#1.2#0"; "RICHTX32.OCX"
 Begin VB.Form frmDocument 
    Caption         =   "Document"
    ClientHeight    =   3600
    ClientLeft      =   60
    ClientTop       =   60
    ClientWidth     =   6225
    Icon            =   "frmDocument.frx":0000
    KeyPreview      =   -1  'True
    LinkTopic       =   "Form1"
    MDIChild        =   -1  'True
    ScaleHeight     =   3600
    ScaleWidth      =   6225
    WindowState     =   2  'Maximized
    Begin RichTextLib.RichTextBox rtfBox 
       Height          =   3315
       Left            =   120
       TabIndex        =   0
       Top             =   240
       Width           =   6000
       _ExtentX        =   10583
       _ExtentY        =   5847
       _Version        =   393217
       Enabled         =   -1  'True
       HideSelection   =   0   'False
       ScrollBars      =   2
       TextRTF         =   $"frmDocument.frx":030A
       BeginProperty Font {0BE35203-8F91-11CE-9DE3-00AA004BB851} 
          Name            =   "Times New Roman"
          Size            =   12
          Charset         =   0
          Weight          =   400
          Underline       =   0   'False
          Italic          =   0   'False
          Strikethrough   =   0   'False
       EndProperty
    End
    Begin VB.Menu mnuFile 
       Caption         =   "&File"
       Index           =   1
       Begin VB.Menu mnuFileNew 
          Caption         =   "&New"
          Shortcut        =   ^N
       End
       Begin VB.Menu mnuFileOpen 
          Caption         =   "&Open..."
          Shortcut        =   ^O
       End
       Begin VB.Menu mnuFileBar0 
          Caption         =   "-"
       End
       Begin VB.Menu mnuFileSave 
          Caption         =   "&Save"
          Shortcut        =   ^S
       End
       Begin VB.Menu mnuFileSaveAs 
          Caption         =   "Save &As..."
       End
       Begin VB.Menu mnuSaveCompressed 
          Caption         =   "Save &Compressed"
       End
       Begin VB.Menu mnuFileSaveAll 
          Caption         =   "Save A&ll"
       End
       Begin VB.Menu mnuFileClose 
          Caption         =   "&Close"
          Shortcut        =   ^E
       End
       Begin VB.Menu mnuFileCloseAll 
          Caption         =   "&CloseAll"
       End
       Begin VB.Menu mnuFileBar1 
          Caption         =   "-"
       End
       Begin VB.Menu mnuFilePrint 
          Caption         =   "&Print..."
          Shortcut        =   ^P
       End
       Begin VB.Menu mnuFilePrintSetup 
          Caption         =   "&PrintSetup"
       End
       Begin VB.Menu mnuFilePrintPreview 
          Caption         =   "&PrintPreview"
          Shortcut        =   ^R
       End
       Begin VB.Menu mnuFileBar3 
          Caption         =   "-"
       End
       Begin VB.Menu mnuFileSend 
          Caption         =   "&Send"
          Begin VB.Menu mnuFileSendEmail 
             Caption         =   "&Email"
          End
       End
       Begin VB.Menu mnuFileExit 
          Caption         =   "E&xit"
          Shortcut        =   {F4}
       End
    End
    Begin VB.Menu mnuEdit 
       Caption         =   "&Edit"
       Begin VB.Menu mnuEditUndo 
          Caption         =   "&Undo"
          Shortcut        =   ^Z
       End
       Begin VB.Menu mnuEditRedo 
          Caption         =   "&Redo"
       End
       Begin VB.Menu mnueditbar2 
          Caption         =   "-"
       End
       Begin VB.Menu mnuEditCut 
          Caption         =   "Cu&t"
          Shortcut        =   ^X
       End
       Begin VB.Menu mnuEditCopy 
          Caption         =   "&Copy"
          Shortcut        =   ^C
       End
       Begin VB.Menu mnuEditPaste 
          Caption         =   "&Paste"
          Shortcut        =   ^V
       End
       Begin VB.Menu mnueditbar3 
          Caption         =   "-"
       End
       Begin VB.Menu mnuEditSelectAll 
          Caption         =   "&SelectAll"
          Shortcut        =   ^A
       End
       Begin VB.Menu mnuEditPloticus 
          Caption         =   "Ploticus"
       End
    End
    Begin VB.Menu mnuView 
       Caption         =   "&View"
       Begin VB.Menu mnuViewToolbar 
          Caption         =   "&Toolbar"
          Checked         =   -1  'True
       End
       Begin VB.Menu mnuViewStatusBar 
          Caption         =   "Status &Bar"
          Checked         =   -1  'True
       End
       Begin VB.Menu mnuViewRuler 
          Caption         =   "&Ruler"
          Checked         =   -1  'True
       End
    End
    Begin VB.Menu mnuFormat 
       Caption         =   "F&ormat"
       Begin VB.Menu mnuFormatFont 
          Caption         =   "&Font..."
       End
       Begin VB.Menu mnuFormatColor 
          Caption         =   "&Color..."
       End
       Begin VB.Menu mnuFormatBullet 
          Caption         =   "&Bullet"
       End
       Begin VB.Menu mnuFormatTabs 
          Caption         =   "&Tabs..."
       End
       Begin VB.Menu mnuFormatParagraph 
          Caption         =   "&Paragraph"
          Begin VB.Menu mnuParagraphLeft 
             Caption         =   "&Left Justified"
          End
          Begin VB.Menu mnuParagraphCentred 
             Caption         =   "&Centred"
          End
          Begin VB.Menu mnuParagraphRight 
             Caption         =   "&Right Justified"
          End
       End
       Begin VB.Menu mnuTypestyle 
          Caption         =   "&Typestyle"
          Begin VB.Menu mnuBold 
             Caption         =   "&Bold"
             Shortcut        =   ^B
          End
          Begin VB.Menu mnuItalic 
             Caption         =   "&Italic"
             Shortcut        =   ^I
          End
          Begin VB.Menu mnuUnderline 
             Caption         =   "&Underline"
             Shortcut        =   ^U
          End
       End
       Begin VB.Menu mnuformatfilebar1 
          Caption         =   "-"
       End
       Begin VB.Menu mnuFormatChangeCase 
          Caption         =   "&ChangeCase"
          Begin VB.Menu mnuFormatChangeCaseLowerCase 
             Caption         =   "&LowerCase"
          End
          Begin VB.Menu mnuFormatChangeCaseUpperCase 
             Caption         =   "&UpperCase"
          End
       End
       Begin VB.Menu mnuFormatFilebar2 
          Caption         =   "-"
       End
       Begin VB.Menu mnuFormatIncreaseIndent 
          Caption         =   "&IncreaseIndent"
       End
       Begin VB.Menu mnuFormatDecreaseIndent 
          Caption         =   "&DecreaseIndent"
       End
    End
    Begin VB.Menu mnuInsert 
       Caption         =   "&Insert"
       Begin VB.Menu mnuInsertObject 
          Caption         =   "&Object..."
       End
       Begin VB.Menu mnuInsertPicture 
          Caption         =   "&Picture..."
       End
       Begin VB.Menu mnuInsertbar1 
          Caption         =   "-"
          Index           =   2
       End
       Begin VB.Menu mnuPloticusPrefab 
          Caption         =   "Ploticus &Prefab"
          Begin VB.Menu mnuPloticusScatter 
             Caption         =   "&Scatter Plot"
          End
       End
       Begin VB.Menu mnuInsertbar3 
          Caption         =   "-"
       End
       Begin VB.Menu mnuInsertTextFile 
          Caption         =   "&TextFile..."
          Shortcut        =   ^T
       End
       Begin VB.Menu mnuInsertDate 
          Caption         =   "&Date"
          Shortcut        =   ^D
       End
       Begin VB.Menu mnuInsertbar2 
          Caption         =   "-"
       End
       Begin VB.Menu mnuInsertSymbols 
          Caption         =   "&Symbols"
       End
    End
    Begin VB.Menu mnuData 
       Caption         =   "&Data"
       Begin VB.Menu mnuEvaluate 
          Caption         =   "&Evaluate"
          Shortcut        =   {F9}
       End
    End
    Begin VB.Menu mnuTools 
       Caption         =   "&Tools"
    End
    Begin VB.Menu mnuWindow 
       Caption         =   "&Window"
       WindowList      =   -1  'True
       Begin VB.Menu mnuWindowNewWindow 
          Caption         =   "&New Window"
          Shortcut        =   {F12}
       End
       Begin VB.Menu mnuWindowBar0 
          Caption         =   "-"
       End
       Begin VB.Menu mnuWindowCascade 
          Caption         =   "&Cascade"
       End
       Begin VB.Menu mnuWindowTileHorizontal 
          Caption         =   "Tile &Horizontal"
       End
       Begin VB.Menu mnuWindowTileVertical 
          Caption         =   "Tile &Vertical"
       End
       Begin VB.Menu mnuWindowArrangeIcons 
          Caption         =   "&Arrange Icons"
       End
    End
    Begin VB.Menu mnuHelp 
       Caption         =   "&Help"
       Begin VB.Menu mnuHelpContents 
          Caption         =   "&HelpContents"
          Shortcut        =   {F1}
       End
       Begin VB.Menu mnuHelpTipoftheDay 
          Caption         =   "&TipoftheDay"
       End
       Begin VB.Menu mnuHelpAbout 
          Caption         =   "&About "
       End
       Begin VB.Menu mnuHelpSpecialThanks 
          Caption         =   "&SpecialThanks"
       End
    End
 End
 Attribute VB_Name = "frmDocument"
 Attribute VB_GlobalNameSpace = False
 Attribute VB_Creatable = False
 Attribute VB_PredeclaredId = True
 Attribute VB_Exposed = False
 Option Explicit

以下是代码。其中一些例程与我们创建实时数学文档的主要目标几乎没有关系。这种情况很常见;当您开发程序时,您会发现一些在您继续进行中需要解决的小问题,以使程序正常工作或仅仅是方便使用。

在 VB 中,Tab 键用于将焦点从一个控件移动到下一个控件,但在编辑文本时,我们通常希望实际插入一个Tab 字符。一种实现方法是为Rich Text BoxKeyDown 事件声明一个事件处理程序。这会检查按键的ASCII 代码是什么,并直接将Rich Text Box 中选定的字符覆盖为一个Tab 字符

 Private Sub rtfbox_KeyDown(KeyCode As Integer, Shift As Integer)
   If KeyCode = 9 Then
     rtfBox.SelText = vbTab
     KeyCode = 0
   End If    
 End Sub

加载文件很简单。只需调用Rich Text BoxLoadFile 方法即可。唯一需要处理的复杂情况是,用户可能尝试打开Rich Text Box 无法处理的文档。这里我们认为这不是程序中的错误,因此不引发错误;相反,我们返回一个状态值:如果成功,则为True,否则为false

 Public Function LoadFile(rsFile As String) As Boolean
   On Error Resume Next
   rtfBox.LoadFile rsFile
   LoadFile = Err.Number = 0    
 End Function

以下是实际执行工作的代码。请注意,所有复杂的操作都在另一个模块中。这是因为我们可以轻松地想象出我们要自动执行这些操作的情况,在这种情况下,我们可能从其他地方获取文本

 Public Sub EvalDoc()
   goEvalDoc.EvalDoc rtfBox
 End Sub

当然,如果无法运行文档,那么没有必要拥有一个方法来重新计算文档,因此我们从数据评估菜单项调用它。查看上面的声明,并看到一个快捷键附加到该菜单项(F9

 Public Sub mnuEvaluate_Click()
   EvalDoc
 End Sub

请记住,当此窗体处于活动状态时,主窗体的菜单不可用,因此我们从自己的文件打开事件处理程序中调用主窗体的文件打开事件处理程序。这就是我们必须从Private 更改为Public 的原因(Friend 也能工作)

 Public Sub mnuFileOpen_Click()
   fMainform.mnuFileOpen_Click
 End Sub

要创建一个全新的文档,我们必须调用主窗体的LoadNewDoc 方法

 Public Sub mnuFileNew_Click()
   fMainform.LoadNewDoc
 End Sub

cEvalDoc.cls

[编辑 | 编辑源代码]

这个类是完成很多艰苦工作的地方。主要方法EvalDoc 看起来非常简单,因为它只是调用了另外三个函数。这些函数

  • 预处理文档,使其成为合法的 JScript,
  • 执行 JScript,
  • 使用结果更新文档。

预处理步骤将宏转换为存储要替换的文本位置的表的 JScript 函数,并将矩阵转换为返回数组的 JScript 函数调用。这使得将数组的值分配给一个变量并以整体的方式处理数组(而不是一次处理一个元素)变得实用。

以下是类的标题。在 Visual Basic IDE 中,您无法看到此文本,但您可以更改其值,因为它们在属性窗口中显示。

 VERSION 1.0 CLASS
 BEGIN
   MultiUse = -1  'True
   Persistable = 0  'NotPersistable
   DataBindingBehavior = 0  'vbNone
   DataSourceBehavior  = 0  'vbNone
   MTSTransactionMode  = 0  'NotAnMTSObject
 END
 Attribute VB_Name = "cEvalDoc"
 Attribute VB_GlobalNameSpace = False
 Attribute VB_Creatable = True
 Attribute VB_PredeclaredId = False
 Attribute VB_Exposed = False
 
 Option Explicit

JScript 源代码的实际评估由MSScript 控件完成。不确定Attibute 是什么。

 Public moScriptControl As MSScriptControl.ScriptControl
 Attribute moScriptControl.VB_VarHelpID = -1

Jscript 解释器只提供 JavaScript 的基本函数,任何不常见的函数都必须由我们提供。我们通过创建一个将这些函数作为方法提供给解释器的对象来实现这一点。

 Private moFunctions As cFunctions

文档评估器对象必须在使用之前进行初始化。这意味着创建一个脚本控件的实例,告诉脚本控件将使用哪种语言,并提供一个全局对象,该对象向解释器添加额外的函数。

 Private Sub Class_Initialize()
   Set moScriptControl = New MSScriptControl.ScriptControl
   With moScriptControl
     .AllowUI = False
     .Language = "JScript"
     .UseSafeSubset = True
   End With
   Set moFunctions = New cFunctions
   moScriptControl.AddObject "Functions", moFunctions, True
   Set moFunctions.oScriptControl = moScriptControl
 End Sub

此类的唯一公共方法是EvalDoc,它以 Rich Text 控件作为唯一参数,处理其中找到的文本,并将答案放回文档中。

 Public Sub EvalDoc(ByRef roDoc As RichTextBox)
   On Error GoTo ErrorHandler
   

首先,我们将文本中找到的所有宏替换为 JScript 函数调用,这些调用将在结果数组中创建结果。

   Dim sScript As String
   sScript = xPreprocess(roDoc.Text)

结果数组是一个动态数组,我们允许它根据需要增长,但我们不会缩小它,因为我们知道我们会再次评估文档,所以释放内存会浪费时间。因此,我们通过简单地将结果计数设置为零来重新初始化数组。计数器也是指向数组中下一个空闲插槽的指针。

   glResultsCount = 0

现在一切都准备好了,我们所要做的就是使用内置的 Eval 函数执行脚本。

   moScriptControl.Run "eval", sScript

最后,如果一切顺利,我们必须将答案放回文档中。

   xUpdateDisplay roDoc
 
   Exit Sub

不幸的是,事情可能会出错,因此我们必须采取措施防止程序崩溃。如果用户在输入宏时漏掉了右括号,则可能会出现问题。用户编写的 JScript 中可能存在普通的语法错误,当然,程序本身也可能存在错误。因此,我们必须有一个错误处理程序。

 ErrorHandler:
   Select Case Err.Number

如果用户在输入宏时漏掉了右括号,预处理器会注意到这一点。在这个原型中,我们通过选择有问题的文本并显示一个消息框来提醒用户来处理这个问题。

     Case ChunkNotTerminated
       roDoc.SelStart = xErrData(Err.Description)(0)
       roDoc.SelLength = xErrData(Err.Description)(1)
       MsgBox "Missing #> at end of macro"

如果问题是宏格式正确但无法识别,我们与语法错误的宏做相同的事情。

     Case UnrecognizedMacro
       roDoc.SelStart = xErrData(Err.Description)(0)
       roDoc.SelLength = xErrData(Err.Description)(1)
       MsgBox "Unrecognized macro, did you mean to display a value?"

因为我们无法预测会发生什么错误,所以我们以一个捕获所有子句结束,该子句通知用户。注意 .ErrorContextxxx 属性;这些属性由预处理器编写的 JScript 函数设置,以便用户可以被引导到发现错误时正在处理的文档部分。

     Case Else
       With moFunctions
         .MakeResult Empty, .ErrorContextStart_, _
                     .ErrorContextEnd_ - .ErrorContextStart_, _
                     SourceError
       End With        
   End Select
 
 End Sub

由于 Visual basic 没有异常,因此我们需要某种方法将信息从引发错误的例程传递到捕获错误的例程。一种简单的方法是将信息打包到一个字符串中并使用 Err 对象的 description 属性。然后,当错误被捕获时,我们可以使用 Split 函数从 description 中获取数据。这个小函数只是包装了 Split 函数,部分原因是为了给它一个有意义的名称,但也因为一开始似乎处理会更加复杂。

 Private Function xErrData(ByRef rsErrDescription As String) As Variant
   xErrData = Split(rsErrDescription, "|")
 End Function

在文档文本可以由 Script 控件评估之前,我们必须确保文本是合法的 JScript。我们通过在字符串中复制文本,查找指令和宏并为它们生成适当的代码来做到这一点。

输出通过调用将变量值添加到输出列表的函数来处理。这些函数需要三个参数:要输出的值、文本中的起始点以及要替换的文本范围的长度。不能期望用户计算行并维护这些值,因此使用宏代替。输出放在宏出现的文本中的位置。与大多数宏替换系统不同,我们不会用输出替换整个宏,因为那样我们就会丢失占位符,并且无法更新它。宏包含三个部分:intro、body、outro。intro 和 outro 保留在文本中,但 body 被新的输出替换。选择在 JavaScript 程序中无法作为合法序列出现的字符序列,并且不太可能出现在文本字符串中很重要。

我选择的序列是 <# #>。

宏可以出现在注释中,并且在那里也能工作。

目前,SplitInstr 用于查找标记,正则表达式可能更好,但我不能确定。这个函数在实践中相当复杂,但基本思路很简单

  • 使用 intro 作为分隔符将文本分成
  • 通过累积 的长度来记录每个 intro 字符串的字符偏移量
  • 删除所有在注释中的文本,
  • 将宏替换为对函数的调用,该函数将命名变量的值以及要插入的位置的开始和长度一起存储,

所有这些都在文本的副本上进行,此时不会干扰富文本框。

 Private Function xPreprocess(rsText As String) As String
   
   Const sINTRO As String = "<#"
   Const sOUTRO As String = "#>"
   Dim aChunks As Variant

使用 intro 序列分割文本。这将导致一个文本块列表(Variant 数组),这些文本块都以宏开头(如果第一个宏之前有任何文本,则列表中的第一个项目除外)。

   aChunks = Split((rsText), sINTRO)
   Dim lChunk As Long

实际执行的文本不需要任何注释,因此我们通过从第一个块中删除单行和多行注释来创建一个新文本。这个块必须特殊处理,因为它违反了所有块都以宏开头的规则。

   xPreprocess = xRemoveComments((aChunks(LBound(aChunks))))
   Dim lStart As Long

为了将结果放置在文本中的正确位置,我们必须跟踪从文本开头到宏的字符偏移量。请注意,我们必须始终显式添加 intro 的长度,因为它不会出现在块列表中。

   lStart = Len(aChunks(LBound(aChunks))) + Len(sINTRO)
   Dim lEnd As Long
   Dim lLenChunk As Long
   Dim lEndVar As Long

现在,我们可以处理每个块并将处理后的文本添加到要执行的字符串中。

   For lChunk = LBound(aChunks) + 1 To UBound(aChunks)

首先,我们必须检查宏是否已终止,在块中搜索 outro。如果缺少 outro,我们会引发错误,因为用户肯定犯了一个错误,忘记了完成宏。有人可能会争辩说,我们应该尝试修补文本并继续。参见 练习

     lEnd = InStr(aChunks(lChunk), sOUTRO)
     If lEnd Then
       Dim sChunk As String
       sChunk = Left$(aChunks(lChunk), lEnd)

现在我们拥有了一个完整的宏,我们必须检查是否识别它。目前只有一种类型的宏,即 显示值 宏。

       lEndVar = InStr(sChunk, "=")
       If lEndVar Then
         xPreprocess = xPreprocess & ";" & vbCrLf _
                     & "show(" & Left$(aChunks(lChunk), lEndVar - 1) _
                     & "," & (lStart + lEndVar) & "," & (lEnd - lEndVar - 1) _
                     & ")" & vbCrLf _
                     & xRemoveComments(Mid$(aChunks(lChunk), _
                                            lEnd + Len(sOUTRO)))
         lStart = lStart + Len(aChunks(lChunk)) + Len(sINTRO)
       Else

如果缺少 = 符号,则这不是 显示值 宏。由于我们还没有定义任何其他宏,这肯定是一个错误,因此我们中止该过程并报告它。

         Err.Raise UnrecognizedMacro, "xPreprocess", _
            lStart & "|" & Len(aChunks(lChunk)) & "|" & "Unrecognized macro type"
       End If
     Else

如果找不到结束块字符,我们会引发错误并中止该过程。

       Err.Raise ChunkNotTerminated, "xPreprocess", _
          lStart & "|" & Len(aChunks(lChunk)) & "|" & "Unterminated chunk"
     End If
   Next lChunk
   
 End Function
 

如果我们删除周围的注释,则将宏转换为代码会更简单。我们可以通过定义一个接受一个块并返回相同块(减去任何注释)的函数来逐个块地执行此操作。

 Private Function xRemoveComments(ByRef rsChunk As String) As String

首先处理最简单的案例:单行注释和完整的多行注释。

   xRemoveComments = xRemoveBracketed(rsChunk, "//", vbLf)
   xRemoveComments = xRemoveBracketed(xRemoveComments, "/*", "*/")
 

现在删除任何前导或尾随[检查拼写] 多行注释片段。这些片段的出现是因为块分割会忽略注释边界。我们在块中搜索结束和开始的多行注释标记。

   Dim lComment As Long
   lComment = InStr(xRemoveComments, "*/")
   If lComment Then
     xRemoveComments = Mid$(xRemoveComments, lComment + Len("*/"))
   End If
   
   lComment = InStr(xRemoveComments, "/*")
   If lComment Then
     xRemoveComments = Left$(xRemoveComments, lComment - 1)
   End If
   
 End Function

此函数重复删除位于给定开始和结束标记之间的子字符串,直到开始标记不再出现在字符串中。

 Private Function xRemoveBracketed(ByRef rsChunk As String, _
                                   ByRef rsStart As String, _
                                   ByRef rsfinish As String) As String
   
   xRemoveBracketed = rsChunk
   Dim lStart As Long
   Do
     lStart = InStr(xRemoveBracketed, rsStart) ' single line
     If lStart Then
       Dim lFinish As Long
       lFinish = InStr(lStart, xRemoveBracketed, rsfinish)

如果结束标记没有出现,则将字符串视为结束标记出现在字符串结尾后的字符处。这允许我们删除没有以换行符序列结尾的单行注释和被宏分割的多行注释。

       If lFinish = 0 Then
         lFinish = Len(xRemoveBracketed) + 1
       End If
       xRemoveBracketed = Left$(xRemoveBracketed, lStart - 1) _
                        & Mid$(xRemoveBracketed, lFinish)
     End If
   Loop Until lStart = 0
   
 End Function


文档评估完成后,我们必须将答案放回文档中的正确位置。乍一看,这似乎是一个简单的任务,因为我们有一系列值以及它们在文档中的位置,因此乍一看,我们似乎需要做的就是枚举结果并将指定的字符范围替换为答案。不幸的是,这将不起作用,除非新文本与原始文本完全相同,因为插入长度不同的新文本会移动要替换的文本,因此结果记录将指向错误的位置。解决方案是按相反顺序枚举结果,以便将连续的结果插入到文档的更靠近开头的位置,这意味着只有已经更新的文本才会移动。

 Private Sub xUpdateDisplay(roDoc As RichTextBox)
   On Error GoTo ErrorHandler
   
   With roDoc

另一个复杂之处是我们希望保留用户的选择。我们不能简单地存储值 .SelStart.SelLength,然后将它们复制回来,因为替换可能发生在选择之前或选择内部,甚至可能发生选择边界位于宏内部的情况。因此,每次执行替换时,我们都必须更新选择。所以我们必须做的第一件事是复制这些属性的值。

     Dim lSelStart As Long
     Dim lSelLen As Long
     Dim lSelNext As Long
     lSelStart = .SelStart
     lSelLen = .SelLength
     lSelNext = lSelStart + lSelLen

我们从最后一个结果记录开始,并向后计数。结果数组从零开始,因此结果计数器始终指向下一个可用插槽。因此,我们在循环开始时递减计数器,当我们完成时,计数器将为零。

     Do While 0 < glResultsCount
       glResultsCount = glResultsCount - 1

要替换富文本框中的文本,我们必须首先设置 .SelStart.SelLength 属性,为了保留用户的选择,我们必须将这些值与用户选择的当前值进行比较,并在必要时更新用户的选择。

       .SelStart = gaResults(glResultsCount).Start
       .SelLength = gaResults(glResultsCount).length
       If .SelStart + .SelLength < lSelStart Then
         lSelStart = lSelStart - .SelLength
         lSelNext = lSelStart + lSelLen
       End If

替换文本是什么取决于结果类型以及尝试计算结果时是否发生错误。如果发生错误,我们将有问题的文本颜色设置为红色并保留它不变。

       Select Case gaResults(glResultsCount).ResultType
         Case SourceError
           .SelColor = vbRed
         Case Else

如果成功,我们将选择颜色设置为黑色并将宏的可替换部分替换为结果。现在我们看到了将结果声明为 Variant 的原因,因为最后我们看到了在这个文档中拥有图表是可能的。实际进行替换的方式取决于结果的类型。如果结果是 标量,那么使用 Visual Basic 的字符串转换函数很容易创建人类可读的表示,但图表却完全不同。因为它们差异很大,所以我们为它们创建了单独的函数。

           .SelColor = vbBlack
           If TypeOf gaResults(glResultsCount).Value Is cPlot Then
             xReplacePlot gaResults(glResultsCount).Value, roDoc
             If .SelStart < lSelStart Then
               lSelStart = lSelStart + 1
               lSelNext = lSelStart + lSelLen
             End If
           Else
             .SelText = xToString(gaResults(glResultsCount).Value)
             If .SelStart < lSelStart Then
               lSelStart = lSelStart _
                         + Len(xToString(gaResults(glResultsCount).Value))
               lSelNext = lSelStart + lSelLen
             End If
           End If
       End Select
     Loop

现在,我们可以再次设置 .SelStart.SelLength 属性来恢复用户的选择。当然,如果它包含一个被替换的宏,那么选择的长度可能与最初的长度大不相同。

     .SelStart = lSelStart
     .SelLength = lSelLen
   End With
   
   Exit Sub

此函数中的错误处理程序不完整,也许是学生的练习。目前,它只是断言了一个假值以停止程序并允许开发人员调试它。在现实生活中,这将非常复杂,因为我们希望处理整个文档。

 ErrorHandler:
   Debug.Assert False
   Resume
 End Sub

由于这实际上是一个 概念证明 而不是一个完成的应用程序,因此我们可以使用一种非常简单的方法将结果转换为文本:只需使用隐式转换。例外情况是 矩阵。矩阵是 JScript 数组,因此我们必须做一些额外的工作来格式化它们。

 Private Function xToString(rvResult As Variant) As String
   
   If TypeName(rvResult) = "JScriptTypeInfo" Then
     ' assume that the object is a JavaScript array
     xToString = xJArrayToString(rvResult)
   Else
     Select Case VarType(rvResult)
       Case vbDouble, vbLong, vbInteger
         xToString = " " & rvResult
       Case vbString
         xToString = " '" & rvResult & "'"
       Case Else
         xToString = rvResult
     End Select
   End If
 End Function

如果结果是图表,则结果实际上是图片文件的名称。为了将它放在文本中,我们必须使用剪贴板插入它,至少这是最简单的方法。如果文件不存在,我们将插入文本来说明这一点,而不是让用户想知道图表在哪里。请注意,当调用此例程时,选择已设置为宏的可替换部分。

 Private Sub xReplacePlot(ByRef rvPlot As Variant, roDoc As RichTextBox)
   
   With roDoc
     If goFSO.FileExists(rvPlot.PicFileName) Then
       .SelText = ""     ' delete the old plot or whatever else there was.
       InsertPicture rvPlot.PicFileName, roDoc
     Else
       .SelText = "File <" & rvPlot.PicFileName & "> does not exist."
     End If
     
   End With
   
 End Sub

如果结果是数组,则我们将使用制表符分隔列,以行和列格式化它。用户必须为文本的该部分设置制表符。不幸的是,我们还没有给用户这样做,这是学生的另一项练习。

 Private Function xJArrayToString(rvResult As Variant) As String
   
   Debug.Assert TypeName(rvResult) = "JScriptTypeInfo"
   Dim oRow As Variant
   
   Dim lRow As Long
   Dim vItem As Variant
   xJArrayToString = vbTab & "["
   For lRow = 0 To rvResult.length - 1
     If lRow <> 0 Then
       xJArrayToString = xJArrayToString & vbTab
     End If
     Set oRow = CallByName(rvResult, lRow, VbGet)
     If TypeName(oRow) = "JScriptTypeInfo" Then
       xJArrayToString = xJArrayToString & vbTab & xJRowToString(oRow)
     Else
       vItem = CallByName(rvResult, lRow, VbGet)
       xJArrayToString = xJArrayToString & vbTab & "[" & vbTab & vItem & "]"
     End If
     If lRow < rvResult.length - 1 Then
       xJArrayToString = xJArrayToString & "," & vbCrLf
     End If
   Next lRow
   xJArrayToString = xJArrayToString & "]"
   
 End Function

每一行实际上都是一个 JScript 数组。JScript 没有多维数组,但由于 JScript 中的一切都是对象,我们可以通过拥有数组的数组来轻松地模拟多维数组。

 Private Function xJRowToString(rvResult As Variant) As String
   
   Debug.Assert TypeName(rvResult) = "JScriptTypeInfo"
   Dim oRow As Variant
   
   Dim lCol As Long
   Dim vItem As Variant
   xJRowToString = "["
   For lCol = 0 To rvResult.length - 1
     vItem = CallByName(rvResult, lCol, VbGet)
     If VarType(vItem) = vbString Then
       vItem = "'" & vItem & "'"
     End If
     xJRowToString = xJRowToString & vItem
     If lCol < rvResult.length - 1 Then
       xJRowToString = xJRowToString & "," & vbTab
     End If
   Next lCol
   xJRowToString = xJRowToString & "]"
   
 End Function

cFunctions.cls

[编辑 | 编辑源代码]

EvalDoc 的预定义函数。此类的实例被提供给 JScript 对象,以提供用于矩阵乘法等操作的全局函数。提供多语言编程示例:应用程序是用 VB 编写的,文档是用 JScript 编写的,JScript 使用的库是用 VB 编写的。

 Option Explicit
   
 Public Enum enumFunctionErrors
   IncompatibleDimensions = vbObjectError + 1
   ChunkNotTerminated
   UnrecognizedMacro
 End Enum
 

ErrorContextxxxx 属性用于使错误处理程序能够确定有问题的源代码的位置。在宏之前,代码中插入了语句来设置这些值。

 Public ErrorContextStart_ As Long
 Public ErrorContextEnd_    As Long
 

某些函数需要能够动态创建变量,因此我们必须提供对正在运行脚本的对象的引用。

 Public oScriptControl  As ScriptControl 
 

Plot 是一个函数,文档可以像调用内置的 JScript 函数一样调用它。它生成一个对象,该对象反过来用于驱动 Ploticus 制图程序。(非常感谢 Steve Grubb <http://ploticus.sourceforge.net>)。

第一个参数是包含要绘制数据的矩阵,第二个参数必须是一个字符串,表示 Ploticus 命令行,除了它不包含 data=datafilename,因为我们使用了一个临时文件,所以该文件会自动提供。

将矩阵写入文件,然后创建一个批处理文件来驱动 Ploticus。执行 Ploticus 文件以创建图片文件。此图片文件的路径存储在 cPlot 对象中,以便以后使用。

注意,Ploticus 对命令行中的空格非常敏感。

这将失败:'pl -gif -prefab scat x=1 y =2 data=12 -o 11

因为 y =2 应该说 y=2,注意多余的空格。

 Public Function Plot(ByRef rvData As Variant, ByRef rsPlotCommands As String) As cPlot
 
   Set Plot = New cPlot
   Plot.PicFileName = TF & ".gif"
   Dim sDataFileName As String
   sDataFileName = xSaveMatrix(rvData)
 
   RunCmd "pl -gif " & rsPlotCommands & " data=" & sDataFileName & " -o " & Plot.PicFileName
   ' @TODO: check output of plot command for errors
 End Function

Ploticus 从文件读取其数据,因此我们必须写入一个文件。我们不关心它叫什么,我们可以在之后删除它,因此我们创建一个唯一的文件名。实际上,可以创建无法正常工作的情况,但这很难。

 Public Function TF() As String
   TF = goFSO.BuildPath(gsTempFolder, Trim$(NextSerial))
 End Function
 

因为 Ploticus 是一个单独的程序,我们必须启动它并等待它完成。此例程是一个通用的命令行程序执行器。

对于每个需要输出文件名作为参数的函数,请使用 $(x) 形式的宏,其中 x 是包含文件名的变量的名称,或者是要接收自动生成的文件名的变量的名称。该变量可以在以后的其他命令中使用。如果该变量在一系列命令中使用,用户无需考虑其实际名称,因为该变量将在首次使用时获得指向临时会话文件夹的唯一字符串值。

如果您需要提供文件作为输入,并且数据保存在变量中,请通过将该字符串与对 SaveData 函数的调用连接起来来构造该字符串;这会将变量写入新的临时文件并返回文件名。

例如,您可以像这样驱动 Ploticus

s=Cmd('pl -gif -prefab scat x=2 y=3 data=' + SaveData(b) + ' -o $f(plot)')

在此示例中,s 从标准输出(如果有)接收输出,名为 plot 的变量命名将接收 Ploticus 图片的文件。如果 plot 变量为空字符串,则将为其创建一个唯一的临时文件名。

如果您想重新使用文件名,请确保先清除它,除非您确实想再次使用同一个名称。

宏是 $x(args),其中 x 是宏的名称,args 是宏以任何形式接受的任何参数。

 Public Function Cmd(ByRef rsCommandLine As String) As cPlot
 
   RunCmd rsCommandLine
   '  ' @TODO: check output of plot command for errors
 
 End Function

Ploticus 需要以特定格式的文件形式提供绘图数据。启动 Ploticus 的例程需要知道该文件的名称。此函数创建文件,将数据写入文件并返回文件名。

 Private Function xSaveMatrix(rvData As Variant) As String
   xSaveMatrix = goFSO.BuildPath(gsTempFolder, Trim$(NextSerial)) & ".dat"
   goFSO.OpenTextFile(xSaveMatrix, ForWriting, True).Write _
        JArrayToPlotData(rvData)
 
 End Function
 

文档中的宏通过调用此函数在 gaResults 数组中创建结果记录。

 Public Sub MakeResult(ByRef rvResult As Variant, _
                       ByRef rlStart As Long, ByRef rlLength As Long, _
                       ByRef reResultType As eResultType)
 
   With gaResults(glResultsCount)

请注意,我们无法使用 TypeOf 来确定结果是否为 JScript 对象,因为根据 VB 的说法,该类型不存在。这非常奇怪,因为 LocalsWatch 窗口会正确显示它,因此 IDE 必须知道。使用 typeName 代替,缺点是字符串比较速度较慢。

     If TypeName(rvResult) = "JScriptTypeInfo" Or IsObject(rvResult) Then
       Set .Value = rvResult
     Else
       .Value = rvResult
     End If
     .Start = rlStart
     .length = rlLength
     .ResultType = reResultType
   End With

学生练习:如果 glresultsCount 大于数组的上界,会发生什么?这里应该怎么做?

   glResultsCount = glResultsCount + 1

 End Sub

Show 函数是公开的,因此 JScript 可以调用它。宏替换过程将这些调用放入源代码中,替换宏。目前,这只是调用 Makeresult 例程。使用包装器的目的是促进更复杂的错误处理。

 Public Function Show(o As Variant, rlStart As Long, rlLength As Long)
 
   MakeResult o, rlStart, rlLength, Result
 
 End Function

将两个 JScript 矩阵相乘并返回另一个 JScript 矩阵。这只是底层矩阵乘法例程的包装器。此包装器将传入的矩阵从 JScript 转换为 VB,并将结果从 VB 转换为 JScript。

 Public Function Multiply(ra1 As Variant, ra2 As Variant) As Object
   On Error GoTo ErrorHandler
 
   Set Multiply = VBMatrixToJArray(xMultiply(JArrayToMatrix(ra1), JArrayToMatrix(ra2)))
 
   Exit Function
 
 ErrorHandler:
   MakeResult Empty, ErrorContextStart_, ErrorContextEnd_, SourceError
 End Function

此函数将两个矩阵相乘。它从公共函数 Multiply 中调用。

矩阵相乘的规则在 矩阵运算 中给出,在 代数 书中。此函数是一个直接的实现,基本上使用与该页面上显示的相同的符号。一个显着的区别是,我们的数组索引从零开始而不是从一开始。

 Public Function xMultiply(ByRef raA() As Double, ByRef raB() As Double) As Double()
   Dim j As Long
   Dim k As Long
   Dim m As Long
   Dim n As Long
   Dim p As Long
   Dim i As Long
   Dim aC() As Double
   Dim cij As Double

请记住,Ubound 函数接受一个可选的第二个参数,该参数指示要返回的维度;第一个维度是数字 1,第二个是数字 2,依此类推。矩阵只有两个维度,第一个是行,第二个是列。

   n = UBound(raA, 2)
   m = UBound(raA, 1)
   p = UBound(raB, 2)
 
   ReDim aC(0 To n, 0 To p)
 
   For i = 0 To m
     For j = 0 To p
       nAcc = 0
       For k = 0 To n
         cij = cij + raA(i, k) * raB(k, j)
       Next k
       aC(i, j) = cij
     Next j
   Next i
 
   xMultiply = aC
 
   Exit Function
 
 End Function
  • 从末尾而不是从开头读取结果列表的策略并不保证对所有文档都适用。你能解释一下为什么吗?提示,想想你可以使用哪些不同类型的 JScript 语句。

modJavaScript

[编辑 | 编辑源代码]

此模块用于帮助将 JavaScript 文档连接到程序内部 Visual Basic 世界的函数。

此实现提供了一些简单的矩阵操作函数。由于这些函数是用 Visual Basic 编写的,并且矩阵必须用 JScript 编写,因此我们需要函数来在 JScript 对象和 Visual Basic 数组之间进行转换。

 Option Explicit

JScript 矩阵实际上是嵌套数组。每行都是一个一维元素数组,矩阵也是一个一维数组,其中每个元素都是一个数组。这意味着 JScript 矩阵可以是 不规则数组。在 Visual Basic 中,通常将矩阵表示为矩形数组。在此实现中,我们只假设数组中的所有行都具有相同数量的元素,因此我们可以通过检查外部 JScript 数组的计数来发现行数,并通过检查第一行的元素计数来发现列数。

此函数将 JScript 数组转换为 Visual Basic 数组。

 Public Function JArrayToMatrix(rvResult As Variant) As Double()
 
   Dim oRow As Variant
   Dim lRows As Long
   Dim lCols As Long

查找行数很容易,因为所有 JScript 对象实际上都是字典,它们都有一个 length 属性。获取行数稍微复杂一些。我们首先必须获取第一个行的 JScript 对象。请记住,JScript 对象实际上是字典,因此数组是字典,其中键是数字。在 Script 控件中,这映射到使每个项目看起来都是对象的属性,因此我们可以使用 CallByName 来检索值。在这种情况下,名称只是 0(即数字零),因为 JScript 数组从 length - 1 编号。

   lRows = rvResult.length
   Set oRow = CallByName(rvResult, 0, VbGet)    
   lCols = oRow.length

现在我们可以分配 Visual Basic 数组。为了避免混淆,我们明确指定了下界和上界;这是一个好习惯,因为它意味着您不必担心文件顶部是否有一个 Option Base 语句。

   ReDim JArrayToMatrix(0 To lRows - 1, 0 To lCols - 1)

现在我们只需枚举行并将内容逐行复制到数组中即可。

   Dim lRow As Long
   Dim vItem As Variant
   For lRow = 0 To lRows - 1
     Set oRow = CallByName(rvResult, lRow, VbGet)
     xJRowToMatrix JArrayToMatrix, lRow, oRow
   Next lRow
   
 End Function

为了提高可读性,从行中复制数据是在单独的例程中执行的。虽然在这种情况下,它并没有增加太多可读性,但它确实有助于清楚地区分行操作和列操作。请注意,这是一个接受对目标数组的 引用行号 的子例程,因为我们无法在 Visual Basic 中分配行。

 Private Sub xJRowToMatrix(raMatrix() As Double, _
                           rlRow As Long, _
                           rvResult As Variant)
   
   Dim lCol As Long
   Dim vItem As Variant
   
   For lCol = 0 To rvResult.length - 1
     vItem = CallByName(rvResult, lCol, VbGet)
     raMatrix(rlRow, lCol) = vItem
   Next lCol
   
 End Sub

从 Visual Basic 数组转换为 JScript 矩阵可以通过创建一段 JScript 源代码并对其进行评估来完成。我们所要做的就是创建一个看起来与用户键入的完全相同的字符串。据推测,直接操作 JScript 对象会更快,如果有人能弄清楚怎么做的话。

注意函数结果的后期绑定。这是因为 Script 控件公开的对象似乎没有实现 Visual Basic 所需的接口。这可能是因为 Script 控件提供了额外的间接层。

 Public Function VBMatrixToJArray(raMatrix() As Double) As Object
   
   Dim lRow As Long
   Dim lCol As Long
   Dim sArray As String
   
   sArray = "["
   For lRow = LBound(raMatrix, 1) To UBound(raMatrix, 1)
     sArray = sArray & "["
     For lCol = LBound(raMatrix, 2) To UBound(raMatrix, 2)
       sArray = sArray & raMatrix(lRow, lCol)
       If lCol < UBound(raMatrix, 2) Then
         sArray = sArray & ","
       End If
     Next lCol
     sArray = sArray & "]"
     If lRow < UBound(raMatrix, 1) Then
       sArray = sArray & ","
     End If
   Next lRow
   sArray = sArray & "]"
   Set VBMatrixToJArray = goEvalDoc.moScriptControl.Eval(sArray)
   
 End Function

cPlot.cls

[编辑 | 编辑源代码]

表示一个绘图。允许 tostring 函数决定做什么来显示绘图。tostring 函数将使用 filename 属性查找 Ploticus 创建的图片文件,并将该图片嵌入到富文本文件中。

 Option Explicit
 Public PicFileName As String

modPlot.bas

[编辑 | 编辑源代码]

此模块提供了与 Ploticus 的底层连接。

 Option Explicit

至少有两种方法可以将图片放入富文本框中。这里选择的方法需要最少的编程,但缺点是它使用 Windows 剪贴板,这很不礼貌,因为用户可能当时在剪贴板中有一些内容。为此,我们使用 SendMessage API 调用,它将 Windows 消息发送到窗口句柄。

 Private Const WM_PASTE = &H302&
 
 Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" _
         (ByVal hwnd As Long, ByVal wMsg As Long, _
          ByVal wParam As Long, lParam As Any) As Long

通过将 Ploticus 创建的图片加载到剪贴板中,然后指示富文本框将其粘贴,插入图片。

 Public Sub InsertPicture(rsFile As String, rtfText As RichTextBox)
 
   Clipboard.Clear
   Clipboard.SetData LoadPicture(rsFile)
   
   SendMessage rtfText.hwnd, WM_PASTE, 0, 0&
 
 End Sub

Ploticus 需要一个以行和列形式存放数据的文件。此数据在我们的文档中表示为矩阵。我们必须将该矩阵转换为字符串,然后将其保存到文件。此函数执行字符串转换部分。

 Public Function JArrayToPlotData(rvResult As Variant) As String
   
   Debug.Assert TypeName(rvResult) = "JScriptTypeInfo"
   Dim oRow As Variant
   
   Dim lRow As Long
   For lRow = 0 To rvResult.length - 1
     Set oRow = CallByName(rvResult, lRow, VbGet)
     If TypeName(oRow) = "JScriptTypeInfo" Then
       JArrayToPlotData = JArrayToPlotData & xJRowToPlotData(oRow)
     Else
       JArrayToPlotData = JArrayToPlotData & " " & CallByName(rvResult, lRow, VbGet)
     End If
     If lRow < rvResult.length - 1 Then
       JArrayToPlotData = JArrayToPlotData & vbCrLf
     End If
   Next lRow
   JArrayToPlotData = JArrayToPlotData
   
 End Function
 Private Function xJRowToPlotData(rvResult As Variant) As String
 
   Dim oRow As Variant    
   Dim lCol As Long
   For lCol = 0 To rvResult.length - 1
     xJRowToPlotData = xJRowToPlotData & CallByName(rvResult, lCol, VbGet)
     If lCol < rvResult.length - 1 Then
       xJRowToPlotData = xJRowToPlotData & " "
     End If
   Next lCol
   
 End Function

JArrayToPlotData 与将 JScript 矩阵转换为 VB 数组的函数非常相似。重写 JArrayToPlotData,使其使用该函数,而不是单独执行从 JScript 的转换。这是否改进了程序?提示:要维护的代码量减少了,还是代码重复(或近似重复)消除了?

modShellWait

[编辑 | 编辑源代码]

JArithmetic 使用外部程序来完成一些比较复杂的工作。这些程序通常是命令行程序,所以我们需要一些包装函数来让程序的其余部分认为它们是内置函数。

这段代码的原始版本在 http://www.msfn.org/board/lofiversion/index.php/t35615.html 上找到。

 Option Explicit
 
 
 Private Const SYNCHRONIZE = &H100000
 Public Const WAIT_OBJECT_0 = &H0
 
 Private Declare Function OpenProcess Lib "Kernel32.dll" _
         (ByVal dwDA As Long, ByVal bIH As Integer, _
          ByVal dwPID As Long) As Long
 
 
 Private Declare Function WaitForSingleObject Lib "kernel32" _
         (ByVal hHandle As Long, _
          ByVal dwMilliseconds As Long) As Long
 

此函数用于执行外部程序。它最多等待十秒钟(10000 毫秒)。不幸的是,它不知道如何处理故障。这留给学生作为练习。

 Public Sub RunCmd(CmdPath As String)
  
   On Error GoTo Err_RunCmd
   
   If ShellWait(CmdPath, 10000) Then
     Stop ' error ' @TODO: do something useful here
   End If
   
   Exit Sub
   
 Err_RunCmd:
   Stop ' do something useful here
 End Sub
 

这是执行命令行并等待调用中指定的时间的低级函数。请参阅 外部进程

 Function ShellWait(CommandLine As String, _
                    TimeOut As Long) As Boolean
    
   Dim ProcessID As Long
   Dim hProcess As Long

Shell 命令返回一个进程 ID,它实际上没有用,因为几乎所有 Windows API 函数都使用进程句柄,但这不是问题,因为OpenProcess API 函数可以在两者之间进行转换。

   ProcessID = Shell(CommandLine)

如果进程 ID 非零,则进程已创建并启动,因此我们可以直接等待它完成。为了等待,我们使用一个名为WaitForSingleObject 的 API 函数,该函数接收进程句柄和以毫秒为单位的超时时间。此函数只是等待进程终止或超时,它返回一个状态代码来指示哪一个。

   If ProcessID Then

非零(True),因此 Shell 成功。现在获取 PID 的进程句柄(Wait 接收句柄)。

     hProcess = OpenProcess(SYNCHRONIZE, False, ProcessID)
     If hProcess Then
       Dim lResult As Long
       lResult = WaitForSingleObject(hProcess, TimeOut)
       If lResult = WAIT_OBJECT_0 Then
         ShellWait = True
       Else
         ShellWait = False
       End If
     Else

未能获取进程句柄。如果进程非常快地终止,或者它可能根本没有执行,即使 Windows 启动了一个进程,也会发生这种情况。向调用者返回false 以表示它失败。

       ShellWait = False
     End If
   Else

如果进程 ID 为零,则 Shell 失败。

     ShellWait = False
   End If
 End Function

启动和项目文件

[编辑 | 编辑源代码]

modMain.bas

[编辑 | 编辑源代码]

主模块包含启动应用程序并使其进入初始状态的所有代码。在本例中,我们还使用它作为声明各种常量和全局函数的地方。


 Attribute VB_Name = "modMain"
   
 Option Explicit

常量gsFILE_FILTERgsSAVE_AS_FILTER 在我们显示“通用对话框”以打开和保存文件时使用。它们告诉对话框在组合框中放置哪些文件掩码。请注意,我们已经为rtftxt 和所有文件提供了支持。这意味着用户可以打开纯文本文件并将其保存为rtf。当然,用户也可以尝试打开不是文本或富文本的文件,我们必须应对这种情况。这些声明是公有的,因为它们实际上是在不同的代码模块中使用的。

 Public Const gsFILE_FILTER As String = "Rich text Format(*.rtf)|*.rtf|Text (*.txt)|*.txt|All Files (*.*)|*.*"
 Public Const gsSAVE_AS_FILTER As String = "Rich Text Format(*.rtf)|*.rtf|Text (*.txt)"

cEvalDoc 类的实例完成了繁重的工作。我们永远不需要超过一个此类的实例,因此我们在这里将其声明为全局变量,并在main 子例程中实例化它。

 Public goEvalDoc As cEvalDoc

处理器需要跟踪文本中进行替换的位置以及实际的替换内容。tResult 用户定义类型 (UDT) 用于保存必要的信息。请注意,Value 成员被声明为Variant,因为表达式的结果可以是任何东西,而不仅仅是数字。

 Public Type tResult
   Value As Variant
   Start As Long
   length As Long
   ResultType As eResultType
 End Type
 

结果类型是一种枚举类型。这使得代码更易于阅读。请注意,没有成员被明确分配值,这是因为我们不关心这些值是什么,只要它们是不同的即可。

 Public Enum eResultType
   SourceError
   SourceNoError
   Result
 End Enum

我们需要一个地方来存储我们将替换回文本中的值,因为在我们完成整个文档的处理之前,我们不能将它们放进去。这样做的原因是结果的长度可能与它替换的文本的长度不同。我们维护结果计数,以便我们可以让gaResults 数组扩展。当我们再次评估文档时,我们首先简单地将glresultsCount 重置为零;这样可以节省时间,因为不需要每次都重新分配结果数组。

 Public gaResults() As tResult
 Public glResultsCount As Long

FileSystemObject 比用于读取文件的旧的内置 VB 函数更易于使用。要使用它,您必须安装脚本运行时库

 Public goFSO As FileSystemObject

此程序中使用的某些函数需要创建临时文件。为了确保避免此程序的不同实例之间以及与其他程序之间的冲突,我们在系统临时文件夹中创建了一个新的临时文件夹,并将它的名称存储在此变量中。

 Public gsTempFolder As String

main 例程初始化应用程序。它创建临时工作区,创建文档评估器的实例,显示主窗体并分配结果数组。

 Sub Main()
   Set goFSO = New FileSystemObject
   
   On Error GoTo ErrorHandler
   
  
   ' Create temporary area for this instance of JArithmetic.
   gsTempFolder = Environ$("TEMP") & "\" & App.EXEName
   If Not goFSO.FolderExists(gsTempFolder) Then
     goFSO.CreateFolder gsTempFolder
   End If
   gsTempFolder = gsTempFolder & "\" & Format$(Now, "yymmddhhmmss")
   If Not goFSO.FolderExists(gsTempFolder) Then
     goFSO.CreateFolder gsTempFolder
   End If
       
   Set goEvalDoc = New cEvalDoc
   
   Load fMainform
   
   fMainform.Show
   
   ReDim gaResults(0 To 100)
   
   Exit Sub
 ErrorHandler:
   Debug.Assert False
   MsgBox Err.Number & ", " & Err.Description & ". Command line = <" & Command$ & ">", vbOKOnly, "Arithmetic"
     Resume
 End Sub

应用程序可以通过多种不同的方式关闭,但每个方式都应以调用此函数结束,以便所有文档都正确关闭。到目前为止,还没有自动保存的实现,也没有提示用户保存已更改的文档。这是添加此类代码的地方。

 Public Sub ExitApplication()
   Dim oForm As Form
   For Each oForm In Forms
     Unload oForm
   Next oForm    
 End Sub
 
 

此例程创建一个唯一的数字。它由需要创建临时文件的函数使用。结果始终是一个整数,但我们使用Double 而不是Long,以便我们“永远”不会用完数字(在这个应用程序中,这种改进几乎没有必要)。

 Public Function NextSerial() As Double
   Static nSerial As Double
   nSerial = nSerial + 1
   NextSerial = nSerial
 End Function

prjJarithmetic.vbp

[编辑 | 编辑源代码]

这是用于将所有这些内容组合在一起的 Visual Basic 项目文件 (VBP)。原则上,应该能够自动下载和编译它。请注意,您在此 VBP 中找到的任何绝对路径在复制到您的计算机后可能无法指向任何内容。

 Type=Exe
 Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#..\..\..\..\..\..\..\WINNT\System32\stdole2.tlb#OLE Automation
 Reference=*\G{420B2830-E718-11CF-893D-00A0C9054228}#1.0#0#..\..\..\..\..\..\..\WINNT\System32\scrrun.dll#Microsoft Scripting Runtime
 Reference=*\G{0E59F1D2-1FBE-11D0-8FF2-00A0D10038BC}#1.0#0#..\..\..\..\..\..\..\ProgramFiles\Hikari\msscript.ocx#Microsoft Script Control 1.0
 Object={F9043C88-F6F2-101A-A3C9-08002B2F49FB}#1.2#0; ComDlg32.OCX
 Object={831FDD16-0C5C-11D2-A9FC-0000F8754DA1}#2.0#0; MSCOMCTL.OCX
 Object={38911DA0-E448-11D0-84A3-00DD01104159}#1.1#0; COMCT332.OCX
 Object={3B7C8863-D78F-101B-B9B5-04021C009402}#1.2#0; RICHTX32.OCX
 Class=cEvalDoc; cEvalDoc.cls
 Form=Fmainform.frm
 Form=frmDocument.frm
 Class=cFunctions; cFunctions.cls
 Module=modMain; modMain.bas
 Class=cPlot; cPlot.cls
 Module=modPlot; modPlot.bas
 Module=modShellWait; modShellWait.bas
 Module=modJavaScript; modJavaScript.bas
 RelatedDoc=..\doc\jarithmetic.htm
 RelatedDoc=..\debug\index.html
 Module=modGPL; ..\..\common\gpl\modGPL.bas
 IconForm="fMainform"
 Startup="Sub Main"
 HelpFile=""
 Title="prjArithmetic"
 ExeName32="Arithmetic.exe"
 Path32="debug"
 Name="prjJArithmetic"
 HelpContextID="0"
 Description="Arithmetic document processor"
 CompatibleMode="0"
 MajorVer=1
 MinorVer=0
 RevisionVer=2
 AutoIncrementVer=1
 ServerSupportFiles=0
 VersionCompanyName="Kevin Whitefoot"
 VersionFileDescription="Embedded JScript document processor."
 VersionLegalCopyright="Copyright Kevin Whitefoot, 2005"
 VersionProductName="JArithmetic"
 CompilationType=0
 OptimizationType=0
 FavorPentiumPro(tm)=0
 CodeViewDebugInfo=0
 NoAliasing=0
 BoundsCheck=0
 OverflowCheck=0
 FlPointCheck=0
 FDIVCheck=0
 UnroundedFP=0
 StartMode=0
 Unattended=0
 Retained=0
 ThreadPerObject=0
 MaxNumberOfThreads=1
 DebugStartupOption=0
上一篇:JArithmetic Round Two 目录 下一篇:语言
华夏公益教科书