会员登录 - 用户注册 - 网站地图 Office中国(office-cn.net),专业Office论坛
当前位置:主页 > 技巧 > Access技巧 > 编程心得绝招 > 经验泛谈 > 正文

让您的用户进行排序

时间:2006-10-22 21:47 来源:Access开发者 作者:Christop… 阅读:
对于这个困扰已久的问题,Chris Weber 开发出一种新的解决方案,允许用户在他们的窗体(或子窗体)中对数据进行排序,您可以轻松地将该解决方案添加到自己的应用程序中(同时可花费更少的时间进行维护)。同时,他还讨论了设计良好的模块应该具有的特征。

“各位!各位!各位!现在是朝廷议事时间。请各位需要奏请国王的大臣站到前面来。”

“我尊敬的国王陛下,”人群中一个声音大胆地进言,“我们的女性顾客询问,她们是否能够以直观的方式和任意组合方式轻松地对窗体上的字段进行排序。”

“这听起来似乎是个不错的请求,”国王答道,“在我的直辖范围内,它肯定会有用。就这么办吧。哪个负责开发的爵士愿意接受这个挑战?”

但是,所有的爵士在面对这样的想法时都退缩了。“尊敬的陛下,”一个声音说道,“女士们已经可以通过无处不在的工具拦对窗体进行排序了。”

“而且,”另一个声音说道,“她们可能会将窗体转换为数据表视图,并且为了便于排序,还会将字段列按照所希望的方式移动。”

“呸!”国王怒吼一声。“难道你没有听到吗?”(因为这些爵士不喜欢倾听。)“女性顾客必须对窗体上的多个字段进行排序。而且你们都知道,在我的领土内,禁止使用数据表视图!难道你想让我的子民去查看隐藏字段和秘密键值吗?他们甚至会丢失字段列,你这个家伙!显然,你们中的一个人会看到该请求中的值。”

然后,Worksalot 先生缓缓地走上前来。“尊敬的陛下,我想接受这个请求。如果可以的话,我想我能够用一上午的时间来完成这项任务。”

“Worksalot 先生,您很勇敢,”国王答道,“但是,由于您总是低估完成请求所需的时间,我决定再多给你半个上午的时间。”

Worksalot 先生得到了国王的赞许,心里很高兴。“我一定会完成的,尊敬的陛下。”

“既然你已经接受了这个任务,请为我们找到一个最好的方法,”国王说道。

“没问题,”爵士答道。他向朝廷鞠了一躬,急忙转过身,然后匆匆离开执行任务去了。

听起来很熟悉吗?有时候,正是这种挑战使事情变得很有趣。当我的一位客户将上述故事中的需求添加到变更列表中时,我认为这种事情可能很合理。但是,在电话里,我无法确保可以成功交付。我说我会“仔细研究”,并为整个变更列表开价,大概需要花费一天中四分之三的时间来考虑这种排序技巧。范围估计可以让我分散整个列表中“产生错误的意向”的风险,但愿,我可以为我的努力争取到合理的回报。

画一张地图……
每当需要发明一些“新”东西时,我总是先出去散散步并进行仔细的考虑。散步通常是我一天中最有收获的一部分。经过周密地考虑特定需求之后,我会总结出一个完备的需求列表:

• 排序界面应该适用于任何数据库中的任何窗体。

• 它还应该适用于子窗体。

• 它需要尽可能方便地实现以完成报价,并且易于在将来进行维护。

• 它应该直观,或者能够令人想起 access 和 Windows 中的其他界面。


我决定使用一个带有两个列表框的对话框。用户可以从窗体打开该对话框,然后从左边的列表中选择字段,并将这些字段移动到右边的列表中,任何顺序均可。在对界面进行原型化时,我意识到,还需要为用户提供一种方法来指定列的排序方式(升序还是降序),因此我在选择按钮下面添加了一个选项组(参见图 1)。这样,用户就可以选择排序顺序,然后将字段从左边移到右边。




图 1


请求
建立起外观之后,我需要解决排序例程的内部工作问题。第一步是,在屏幕上用户调用对话框的位置显示对话框。这意味着,窗体上需要有一个用于打开对话框的按钮,或者一个用于检索字段并打开对话框的全局可用方法。我选择后一种方法以便于实现(我希望不必更改可以使用排序工具的每个窗体,这样,在此过程中就不会丢失窗体上的屏幕空间)。我决定创建一个自定义的快捷菜单,该菜单具有调用公共函数的选项。该菜单如 图 2 所示,图中还显示自定义 Multifield Sorting 项的属性表。




图 2


菜单项调用自定义函数需要两个步骤。您需要创建一个菜单项,然后为它的 On Action 属性分配自定义函数。创建菜单的第一步看起来可能不太直观:我根据自定义菜单对话框改写了一个现有的命令。我经常将一个虚拟宏拖到菜单栏上,然后根据需要更改它的名称和按钮图像。

创建菜单之后,分配函数就很简单了。我只需用函数的名称重写任何现有的 On Action 菜单项,并在前面添加一个等号。On Action 项有三个特征:它必须是一个函数而非子例程,该函数不能返回值,并且其中必须包含括号。以下就是我用来打开排序对话框的项:

=OpenMultiFieldSorting()

现在,当用户从菜单中选择 Multifield Sorting 时,该菜单项将调用 OpenMultiFieldSorting 函数,这将打开我的对话框。以下代码即为该函数:

Function OpenMultiFieldSorting()
  DoCmd.OpenForm "fdlgMultiFieldSorting", _
                    windowmode:=acDialog
End Function

这种单点耦合允许我们将对功能的平衡封装在对话框本身内,从而确保其内聚性和可维护性。说得太有道理了!但我相信,开发人员在编写函数时应该始终关注以下四个方面:

• 耦合是指任何界面或代码的入口点。例程之间的交互点应该保持为最小值(这称为“松耦合”设计)。这就是尽量避免使用全局变量的原因。每个全局变量都可能成为例程和项目其余部分之间的隐藏耦合点。理想情况下,您的函数应该与外界只有一个耦合点。这样,就可以控制信息的流入/流出,并且可以避免无意中产生的副作用。

• 封装 是指例程将所有所需功能包装在一起,并使其对调用程序隐藏的功能特定于对象的代码保留在其中。封装与耦合密切相关 — 与例程的交互点越多,您必须了解的有关例程及其工作方式的内容就越多;交互点越少,您必须了解的有关所调用例程的内容也就越少。

• 内聚性 是一个模糊的术语。内聚代码例程或对象只执行一个函数(或尽可能少的函数)— 就足以使例程起作用。已到了一心一意的程度。内聚例程在整个系统中既有用又可以重用。例程所完成的任务越多,越有可能在应用程序调用它时做得“过多”。

• 可维护性 真正地总结了前面三个特征。包含前面三个原则的代码例程和对象更易于维护、修改、调试并提供给他人。您的生活也因此而轻松起来。


现在,我已经为例程建立了单入口点,任何窗体或子窗体都能够调用排序对话框,方法是将其 Shortcut Menu 属性设置为自定义快捷菜单。现在,要是我的对话框能够做一些事情该多好……

有许多要排序的字段
下一步是用所使用的窗体或子窗体中的字段填充对话框左侧的列表框 (lstFields)。填充列表框有多种方式。您可以使列表基于表格、值列表,或使用回调函数来填充列表(即,向对话框传递对函数的引用,并让对话框调用该函数)。我避免使用回调函数是因为它们不是基于对象的,并且使用起来相对比较复杂(而且解释起来也相对复杂,您可能已经注意到,我的定义比较含糊。)我经常使用包含由某种分隔符(通常是逗号)进行分隔的值的字符串来传递数据。但对于该界面而言,我需要将列表作为字段集合来处理,并使用大量字符串操作函数来添加或删除列表中的字段名。

我选择了一个简单的系统表。这并不是最佳的解决方案:实际上,该表是应用程序和排序例程之间的另一个接触点。我需要使其有利可图,并为了达到这个目标而需要一个简单的实现。系统表还能够以可视方式检查代码的行为,只需打开该表即可。

zstblMultiFieldSorting 表的设计及其四个字段如图 3 所示。ListName 字段允许您使用对话框左侧的 lstFields 列表框和右侧的 lstSort 列表框的表。FldName 字段具有唯一的索引,这样相同的字段名就不会出现两次。AscDesc 字段表示排序的顺序,并且实际上只用于移到右侧 lstSort 列表框的字段(直到选择某字段进行排序,否则不会有排序方向)。如您所见,SortOrder 字段还可以用于 lstSort,这样我就可以为窗体创建一个 OrderBy 字符串了。




图 3


填充表格后,列表框就可以对表格进行查询以获得它们的字段列表。每个列表框的 SELECT 语句都会检索表中列出的字段:

SELECT FldName
FROM zstblMultiFieldSorting
WHERE ListName="lstFields"
ORDER BY FldName

SELECT FldName, AscDesc
FROM zstblMultiFieldSorting
WHERE ListName="lstSort"
ORDER BY SortOrder

连接到窗体
通过在对话框的 Form_Open 事件中使用 Screen.ActiveForm,我希望从调用窗体的 RecordsetClone 中获得字段的列表。这运行起来应该比较可靠,因为在对话框打开时 ActiveForm 仍旧是调用窗体。但是,这实际上比我想象的更神奇,因为 ActiveForm 只指向主窗体,而不指向子窗体。子窗体永远都不是屏幕上的活动窗体。我本来可以使用 Screen 对象,该对象不仅返回对活动窗体的引用,还返回对活动控件的引用(SubForm 控件中保存一个子窗体)。通过查看控件的 Parent.Form 属性,我可以获得字段列表。我的最初实现使用了 Screen.ActiveControl 来获得所需窗体的句柄,然后从其 RecordsetClone 获得各个字段:

Dim frm as Form
Dim fld as Field

Set frm = Screen.ActiveControl.Parent
For Each fld In frm.RecordsetClone.Fields
...

上述内容运行起来很顺利,但最后当我试图使用 Northwind 数据库中 Employees 窗体上的对话框时,却遇到了一个错误。当控件出现在选项卡式的界面上时,它们的直接父控件是选项卡控件,而不是窗体本身。当我试图保存对选项卡控件的引用时,对窗体变量的赋值失败了。

不知何故,我必须沿着父层次结构进行寻找,直到遇到一个窗体。使这个问题更加有趣的是,Access 对象没有报告其对象类型的方法。我的解决办法是使用一个简单的循环,直到一个具有“HasModule”属性的对象跳到最前端。控件并没有模块,因此,我遇到的第一个具有该属性的对象一定是具有焦点的控件的父窗体。我的最终实现使用了 HasProperty 函数,我经常在 access 数据库中使用这个方便的函数。如果对象具有一个特定的属性,HasProperty 会返回 True,如果对象没有特定属性,HasProperty 返回 False。

以下是这段代码,开头是模块级的变量,用于在模块的例程间传递信息。对话框的 Open 事件中有一段关键的代码,它删除了表格中的所有记录。然后,代码循环访问组成窗体记录集的所有字段(我通过窗体的 RecordsetClone 属性检索窗体的记录集)。但是,仅仅因为字段出现在记录集中,并不意味着字段实际上在窗体中显示。因此,代码随后循环访问调用窗体上的控件,如果控件正在使用 RecordsetClone 中的一个字段,并且该字段可见,那么代码会将该字段添加到表格,并进行标记以备 lstFields 列表框使用:

Dim frm As Form 'the calling form or subform
Dim rst As Recordset 'recordsource for the listboxes
Dim iSortOrder As Integer

Private Sub Form_Open(Cancel As Integer)
On Error GoTo ErrorHandler
Dim fld As Field, ctl As Control, obj As Object
  
  Set obj = Screen.ActiveControl.Parent
  Do Until HasProperty(obj, "HasModule")
    Set obj = obj.Parent
  Loop
  
  Set frm = obj
  CurrentDb.Execute _
   "DELETE FROM zstblMultiFieldSorting", dbFailOnError
  Set rst = CurrentDb.OpenRecordset( _
         "zstblMultiFieldSorting", dbOpenDynaset)
  For Each fld In frm.RecordsetClone.Fields
    For Each ctl In frm
      If HasProperty(ctl, "controlsource") Then
        If ctl.ControlSource = fld.Name Then
          If ctl.Visible Then
            rst.AddNew
              rst!ListName = "lstFields"
              rst!FldName = fld.Name
            rst.Update
          End If
        End If
      End If
    Next ctl
  Next fld
  lstFields.Requery
  lstSort.Requery
  
Exit_Here:
  Exit Sub

ErrorHandler:
MsgBox "Error #" & Err.Number & ": " & _
  Err.Description & " by " & Err.Source, _
  vbOKOnly, "Error in procedure Form_Open"
  Resume Exit_Here

End Sub

Function HasProperty(pobj As Object, _
       pstrName As String) As Boolean
On Error Resume Next
Dim strProperty As String

  strProperty = pobj.Properties(pstrName).Name
  HasProperty = (Err = 0)
  Err.Clear
  
End Function

对可见控件的检查是我在初始实现后添加的新功能。出于相同的原因,我认为让用户将窗体切换到数据表视图不是个好主意,同样,让用户对基础查询中的隐藏字段(如键值)进行排序,或者对连接到隐藏控件的字段进行排序也不是个好主意。我在循环中添加了几个嵌套的 If 语句,它们可以迭代通过调用窗体上的控件。第一个 If...Then 又一次使用 HasProperty() 来确定该控件是否具有一个控件源。只有在这时,我才将字段名与窗体的 RecordsetClone.Fields 集合相比较。

如果您想使用该函数,那么在窗体的基础查询中为 Select 语句中的字段提供有意义的名称是个不错的主意。我认为用户都不希望看到诸如 EmpFirst、EmpLast 这样的字段名。

做出选择
用户可以选择字段进行排序,方法是单击对话框中的四个选择按钮:

• Add — 将一个字段从左边的列表框复制到右边的列表框。

• Add all — 将所有字段从左边的列表框复制到右边的列表框。

• Remove — 删除一个移动到右边列表框中的字段。

• Remove all — 删除右边列表框中的所有字段。


每个按钮都使用与 zstblMultifieldSorting 关联的模块级记录集,以编辑表的内容并查询列表框。以下是上述四个按钮的代码,为了使该过程便于阅读,我们删除了错误处理部分(您可以在下载文件的数据库中看到完整的代码):

Private Sub cmdAdd_Click()

  If lstFields.ItemsSelected.Count Then
    With rst
      .FindFirst "FldName='" & lstFields.Value & "'"
      If Not .NoMatch Then
        iSortOrder = iSortOrder + 1
        .Edit
          !ListName = "lstSort"
          !AscDesc = AscDesc()
          !SortOrder = iSortOrder
        .Update
      End If
    End With
  End If
  lstFields.Requery
  lstSort.Requery

End Sub

Private Sub cmdAddAll_Click()
Dim fContinue As Boolean

  fContinue = True
  With rst
    .MoveFirst
    Do While fContinue
      .FindFirst "ListName='" & "lstFields" & "'"
      If Not .NoMatch Then
        .Edit
          iSortOrder = iSortOrder + 1
          !ListName = "lstSort"
          !AscDesc = AscDesc()
          !SortOrder = iSortOrder
        .Update
      Else
        fContinue = False
      End If
    Loop
  End With
  lstFields.Requery
  lstSort.Requery
  
End Sub

Private Sub cmdRemove_Click()
  
  If lstSort.ItemsSelected.Count Then
    With rst
      .FindFirst "FldName='" & lstSort.Value & "'"
      If Not .NoMatch Then
        .Edit
          !ListName = "lstFields"
          !AscDesc = Null
          !SortOrder = Null
        .Update
      End If
    End With
  End If
  lstFields.Requery
  lstSort.Requery

End Sub

Private Sub cmdRemoveAll_Click()

  With rst
    .MoveFirst
    Do While Not .EOF
      .Edit
        !ListName = "lstFields"
        !AscDesc = Null
        !SortOrder = Null
      .Update
      .MoveNext
    Loop
  End With
  lstFields.Requery
  lstSort.Requery
  iSortOrder = 0

End Sub

这里唯一的技巧是为 lstSort 保留 SortOrder。我可以在代码中使用一条 SQL 语句直接对表进行操作,但由于我必须在 Form_Open 事件中使用记录集来加载表,所以只需使其可用于选择例程。保持记录集存在可以极大地简化增量和减量 iSortOrder。Add 按钮还调用一个简单的 AscDesc() 函数,该函数根据用户的选项组选择返回 ASC 或 DESC。图 4 显示的 zstblMultifieldSorting 是根据图 1 中 Employees 窗体选择进行排序的结果。




图 4


排序
现在,我已经可以检索和选择窗体上的可见字段,并指定其排序顺序,接下来需要将选择应用到调用窗体。cmdApply 的单击事件将该排序应用于调用窗体或子窗体。代码首先将记录集的 Sort 属性设置为 SortOrder 字段。因为我已经使用 OpenRecordset 方法指定希望使用一个动态集(可以提供更好的性能,但是只包含指定记录的主键),因此设置 Sort 属性不会自动对记录进行排序。但是,我需要做的全部事情就是,根据第一条记录创建一个新的记录集 (rstSorted)。

对记录集进行排序以后,我循环访问各个记录,并根据 lstSort 中出现的字段创建一个排序顺序字符串。循环结束后,如果 strOrderBy 具有长度,我会剪掉尾部的逗号-空格组合,并将其应用到调用窗体。以下是一段代码,为了便于阅读,同样没有包含注释和错误处理:

Private Sub cmdApply_Click()
Dim strOrderBy As String, rstSorted As Recordset

  rst.Sort = "SortOrder"
  Set rstSorted = rst.OpenRecordset
  With rstSorted
    .MoveFirst
    Do While Not .EOF
      If !ListName = "lstSort" Then
        strOrderBy = strOrderBy & !FldName & " " _
              & !AscDesc & ", "
      End If
      .MoveNext
    Loop
  End With
  If Len(strOrderBy) Then
    strOrderBy = Left(strOrderBy, Len(strOrderBy) - 2)
    frm.OrderBy = strOrderBy
    frm.OrderByOn = True
  End If
  
Exit_Here:
  rstSorted.Close
  DoCmd.Close acForm, Me.Name
  Exit Sub

End Sub

要确保排序对话框不会对使用它的应用程序带来影响,需要做一些清理工作。清理代码在对话框的 OnClose 事件中,它关闭了 rst 记录集并将其设置为空。一旦对话框关闭,用户就可以看到窗体按照需要进行了排序。

关于该技术,有两点注意事项。对话框依赖于具有获得焦点的控件的窗体。因此,如果您已经将窗体上的所有控件锁定为只读,则必须至少添加一个可以获得焦点的控件。此外,如果应用于子窗体,用户必须首先单击子窗体,以便子窗体上的控件可以获得焦点。

我认为,为了获得这种方便的功能,这两点小小的牺牲是值得的。要在您的数据库中使用这种技巧,只需导入 zstblMultiFieldSorting 表、fdlgMultiFieldSorting 窗体,并如前所述复制或编写一个入口点函数。不要忘记,您需要一个自定义菜单来调用入口点函数,或者需要使用其他方法从窗体中打开 fdlgMultiFieldSorting。

回家
“各位!各位!各位!朝廷议事时间。Worksalot 先生,"

“到前面来,尊敬的爵士,”国王说道。“怎么样?你找到对字段进行排序的方法了吗?”

“我找到了,尊敬的陛下,”爵士回答。“现在,您的所有子民都可以对窗体的多个字段进行排序,升序降序都可以,也可以为其任意组合排序。”

“太好了!”国王答道。“我知道你能行。请如实告诉我,你是如何找到最好的办法的?”

“我恐怕没有成功,尊敬的陛下。我花了一个半上午的时间来研究排序,然后又花了一些时间,但都没有成功。”

“拖出去斩首!”

(责任编辑:admin)

顶一下
(0)
0%
踩一下
(0)
0%
发表评论
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
评价: