-- 编辑:这现在是如何在这个问题的上下文中可靠地移动工作表的更大问题的一部分--
(注意:在准备这篇文章和测试解决方案的过程中,我可能已经回答了我自己的问题。只是张贴这个,希望比我更聪明的人能想出一些东西。无论如何,我猜它仍然是未来搜索者的一个很好的资源。)
我为我的一位客户制作了一个 Excel 解决方案,其中包含大量 VBA。因此,我很自然地签署了 VBA 代码,因此我的客户不会收到宏安全消息。但是,此解决方案所做的一件事是在同一工作簿中制作模板表的副本。模板表位于其代号上,从那时起,该表的所有副本都通过其代号来识别(具有尾随序列 nr.) - 它们需要稍后再次识别和处理。
乍一看很无辜,但是当我演示解决方案并试图保存它时,我立即得到:
“您修改了一个已签名的项目。您没有正确的密钥来签署此项目。签名将被丢弃。”
之后签名被丢弃,并在重新打开宏安全提示时发挥作用。印象不好:(
代码的简化形式如下:
Worksheet.Copy用于制作此工作表副本的VBA 代码(并修改副本,但这与此处无关);当我在没有我的证书的机器上手动执行相同的操作时,我得到了相同的体验。(一个教训:在演示任何东西之前,总是在真正的空白系统上进行测试......)
我对此进行了一些搜索(参见例如ozgrid.com和answers.microsoft.com),虽然很少有人遇到这个问题,但这似乎是一种不可避免的事情。我怀疑其背后的原因是这样的:
根据ozgrid.com上的帖子,这也会发生在删除工作表时,如上文所述。它还建议在没有打开 VBA IDE 的情况下创建新工作表不会触发此操作,删除这些新工作表也可以。但是,一旦您转到 VBA IDE,当前存在的所有工作表都会再次变为“不可删除”。
我怀疑当您在没有打开 VBA 编辑器的情况下添加新工作表时,Excel 添加了一个真正没有添加 VBA 模块的工作表,因此项目哈希不会更新。因此,出于同样的原因,这些工作表也可以被删除。依次打开 VBA 编辑器使 IDE 查询工作簿中的模块,此时这些仍然缺失的模块被创建,将它们的存在烘焙到哈希中,这反过来也使它们不可复制,因为它们的 VBA 占用空间已变为非零.
现在 1,000,000 美元的问题是:我们如何解决这个问题?这个网站上有一些聪明人,所以也许我们可以想出一个开箱即用的解决方案?
一个使用细节将使这更容易(至少对我而言):客户是唯一添加工作表的人,他永远不会进入 IDE。不过,如果我不会因为忘记进入 IDE 而无意中搞砸了构建,那就太好了。
我已经尝试了几种可能的解决方案,在带有我签名的计算机上创建它们,并在没有我签名的计算机上测试它们。目前,我仅使用 32 位 Excel 2010 进行这些测试,因为这就是我所拥有的全部,而且这也是我和我的客户最感兴趣的版本。
通过 IDE 从模板表中删除所有 VBA 代码,因此它对哈希没有贡献。
要是它这么简单就好了……这行不通,所以可能模块本身的存在和/或它的元数据(就像它的名字)也被散列了,这听起来并没有不合理。或者,您根本无法删除所有 VBA 代码,因为 IDE 倾向于始终附加一个空行(因此,单个 CrLf 尽可能地为空,尽管它CodeModule.CountOfLines返回 0)。或者整个 VBA 代码模块的内容被检索和散列,这样终止的 NULL 字符或前导 0 字节计数有助于散列。无论如何,这里没有运气。
作为测试,我添加了一个宏来说明有哪些 VBA 模块,以及它们包含多少行。使用这个,“清空”模板表的直接副本仍然有 0 行,但签名丢失,而新插入的表显示在 VBModules 集合中,甚至有 2 行(默认值Option Explicit),但签名仍然在保存...
但是 Excel 可能只是比我们更聪明,因为 2 行Option Explicit是虚拟的,甚至 VBA 模块的存在首先是虚拟的。当我使宏还列出所有带有代码名称的工作表时,结果这些“安全”工作表的代码名称为空(0 长度字符串),实际上表明它们根本没有代码模块。
而是创建一个全新的工作表,并且只复制模板工作表的内容。
虽然这确实有效,但对我来说似乎有点不确定;我不相信仅仅sourceSheet.Cells.Copy destSheet.Cells将复制绝对一切,用户可以在它扔...我宁愿从而继续使用内置的Worksheet.Copy功能是安全的,并没有特殊的代码编写桩每一个可能的细节。
举个例子:sourceSheet.Cells.Copy destSheet.Cells例如,复制特定于工作表的命名范围,但显然只有当它们实际用于工作表本身时。未引用的名称在副本中消失了!谈谈我必须写的特殊情况复制代码......
然后是复制的工作表根本没有分配任何代号,我目前需要识别它们。
创建一个新的临时工作簿,Worksheet.Copy将工作表放在那里,记下它的名称,将其显式保存为 .xlsx 文件以摆脱任何 VBA 模块,关闭并重新打开临时工作簿以摆脱任何旧的内存碎片,按名称再次找到它,然后将Worksheet.Move其返回到源工作簿。
这有效!如果没有重新打开实际的工作簿,它不会重新打开,所以我猜内存中的表示不能很容易地“擦洗”而不会造成任何伤害。
但是......新工作表再次完全没有代码名称,甚至更多:我不喜欢这张工作表移动到不相关的工作簿;虽然在快速测试中,对原始工作簿中其他工作表的任何引用都被保留了(甚至没有扩展到包括工作簿名称或路径!),但我对此仍然有点不安......谁知道用户是什么类型的内容可能会扔它...
<Paranoid mode="on">谁知道里面会有什么类型的机密信息,我不想为这些信息最终在他们不知情的情况下从 Temp 文件夹中泄漏负责。</Paranoid>
创建一个新的、空的、临时表以及Worksheet.Copy模板,然后用临时表的 VBA 模块替换真实副本的 VBA 模块。或者只是将 VBA 模块视为一个整体。
我只是想不出办法来做到这一点。VBA 本身似乎不会让您这样做,而且我不希望我的客户仅为此打开“允许访问 VB 项目”选项。我怀疑我是否能够做到这一点,在我再次对代码模块进行核攻击之前,已经造成了损害。
创建一个仅对我(开发人员)可见的宏,通过解决方案 2 或 3 创建模板表的完美副本,并丢弃原始模板表,将其替换为 VBA 擦洗的副本。被我用作交付给客户之前的最后一步。
解决方案 2 的警告在这里不太重要,因为当我进行新版本交付时,我自己知道模板表上的内容,因此完美副本所需的代码量很少,并且可以控制。但是 3 似乎更安全,更容易......我必须选择一个。
由于我仅通过shtTemplate.直接使用而不是 来访问其 VBA 代码名称上的模板表ThisWorkbook.Worksheets("Template").,这显然使 Excel 无法在运行中进出切换它变得过于复杂。到目前为止,我所有的尝试要么失败,要么只是让 Excel 崩溃。那里没有爱:(
我再次尝试通过操作在第二个 Excel 中加载的副本设置为msoAutomationSecurityForceDisable,从而避免正在运行的 VBA 主机被破坏,并且在几乎每次更新后保存并重新打开。但这也无济于事,在打开已清理的工作簿时出现“自动化错误 - 灾难性故障”之类的错误,或者严重损坏新工作簿(ThisWorkbook项目资源管理器中的每个工作表模块都使用派生名称复制该模块)。
重新编写所有 VBA 以不使用硬编码模板表的代码名称,而是将此名称存储在设置表中,然后应用上述解决方案 5。
代码终于可以工作了,甚至不必使用第二个暂存 Excel;没有崩溃或损坏!但是此代码仅在我终生无法获得代码以再次为擦洗过的工作表提供有效代码名称的范围内有效;它仍然是一个零长度的字符串。也没有运行时错误表明这一点。当我在此期间打开 IDE 时,代码名称设置正确。
这让我相信在你的工作表上有一个代号意味着它有一个非空的代码模块,这意味着它与数字签名混淆。事后看来,这并不出乎意料。
这让我相信没有任何办法可以创建一个模板表:
Worksheet.Copy不丢失签名的情况下可以安全地复制,并且到目前为止,我看到的唯一解决方案是确实使用擦洗过的模板表,以便能够使用Worksheet.Copy, 但通过除代码名称之外的其他方式来查找和识别它及其结果表。上面有一个用户隐藏的部分,我可能会在其中添加“这是模板/副本”状态,尽管它让我内心的完美主义者感到畏缩。
但是,如果有人想尝试,最好有更多选择!我可以在需要时发布代码示例。
有很多东西需要考虑,我并不认为这个答案会解决你所有的问题。但我曾经编写过一个名为 SoftLink 的函数,它最多需要 4 个参数 (i) Boolean: CellRef (or NamedRange) (ii) String: Range (iii) String: WorksheetName (iv) String: WorkbookName 这会破坏与任何内容的任何链接单元格,然后解析 VBA 代码中的字符串参数。
毫无疑问,这种方法会影响性能,但它是解决链接地狱的一种方法。
调用公式的示例
=softlink(FALSE,"Foo")
=softlink(TRUE,"C4","Sheet1","Book2")
=softlink(TRUE,"D5","Sheet2")
Run Code Online (Sandbox Code Playgroud)
我已经凭记忆想出了一个实现。我对错误有恐惧症......所以请原谅子例程中的一些奇怪的循环。
Option Explicit
Function SoftLink(ByVal bIsCell As Boolean, ByVal sRangeName As String, _
Optional sSheetName As String, Optional sBookName As String) As Variant
Dim vRet As Variant
If Len(sRangeName) = 0 Then vRet = "#Cannot resolve null range name!": GoTo SingleExit '* fast fail
Dim rngCaller As Excel.Range
Set rngCaller = Application.Caller
Dim wsCaller As Excel.Worksheet
Set wsCaller = rngCaller.Parent
Dim wbCaller As Excel.Workbook
Set wbCaller = wsCaller.Parent
Dim wb As Excel.Workbook
If Len(sBookName) > 0 Then
vRet = FindWorkbookWithoutOnErrorResumeNext(sBookName, wb)
If Len(vRet) > 0 Then GoTo ErrorMessageExit
Else
Set wb = wbCaller
End If
Debug.Assert Not wb Is Nothing
Dim ws As Excel.Worksheet
If Len(sSheetName) > 0 Then
vRet = FindWorksheetWithoutOnErrorResumeNext(wb, sSheetName, ws)
If Len(vRet) > 0 Then GoTo ErrorMessageExit
Else
Set ws = wsCaller
End If
Dim rng As Excel.Range
If bIsCell Then
vRet = AcquireCellRange(ws, sRangeName, rng)
If Len(vRet) > 0 Then GoTo ErrorMessageExit
Else
vRet = AcquireNamedRangeWithoutOERN(ws, sRangeName, rng)
If Len(vRet) > 0 Then GoTo ErrorMessageExit
End If
SoftLink = rng.Value2
SingleExit:
Exit Function
ErrorMessageExit:
SoftLink = vRet
GoTo SingleExit
End Function
Function AcquireCellRange(ByVal ws As Excel.Worksheet, ByVal sRangeName As String, ByRef prng As Excel.Range) As String
On Error GoTo FailedCellRef
Set prng = ws.Range(sRangeName)
SingleExit:
Exit Function
FailedCellRef:
AcquireCellRange = "#Could not resolve range name '" & sRangeName & "' on worksheet name '" & ws.Name & "' in workbook '" & ws.Parent.Name & "'!"
End Function
Function AcquireNamedRangeWithoutOERN(ByVal ws As Excel.Worksheet, ByVal sRangeName As String, ByRef prng As Excel.Range) As String
'* because I do not like OERN
Dim oNames As Excel.Names
Dim bSheetScope As Long
For bSheetScope = True To False
Set oNames = VBA.IIf(bSheetScope, ws.Names, ws.Parent.Names)
Dim namLoop As Excel.Name
For Each namLoop In oNames
If VBA.StrComp(namLoop.Name, sRangeName, vbTextCompare) = 0 Then
Set prng = ws.Range(sRangeName)
GoTo SingleExit
End If
Next
Next
ErrorMessageExit:
AcquireNamedRangeWithoutOERN = "#Could not resolve range name '" & sRangeName & "' on worksheet name '" & ws.Name & "' in workbook '" & ws.Parent.Name & "'!"
SingleExit:
Exit Function
End Function
Function FindWorksheetWithoutOnErrorResumeNext(ByVal wb As Excel.Workbook, ByVal sSheetName As String, ByRef pws As Excel.Worksheet) As String
'* because I do not like OERN
Dim wsLoop As Excel.Worksheet
For Each wsLoop In wb.Worksheets
If VBA.StrComp(wsLoop.Name, sSheetName, vbTextCompare) = 0 Then
Set pws = wsLoop
GoTo SingleExit
End If
Next wsLoop
ErrorMessageExit:
FindWorksheetWithoutOnErrorResumeNext = "#Could not resolve worksheet name '" & sSheetName & "' in workbook '" & wb.Name & "'!"
SingleExit:
Exit Function
End Function
Function FindWorkbookWithoutOnErrorResumeNext(ByVal sBookName As String, ByRef pwb As Excel.Workbook) As String
'* because I do not like OERN
Dim wbLoop As Excel.Workbook
For Each wbLoop In Application.Workbooks
If VBA.StrComp(wbLoop.Name, sBookName, vbTextCompare) = 0 Then
Set pwb = wbLoop
GoTo SingleExit
End If
Next wbLoop
ErrorMessageExit:
FindWorkbookWithoutOnErrorResumeNext = "#Could not resolve workbook name '" & sBookName & "'!"
SingleExit:
Exit Function
End Function
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1288 次 |
| 最近记录: |