使用 WPF 风格架构的工作表窗体





5.00/5 (1投票)
本教程将为您展示一个 WPF 风格框架的良好起点,该框架旨在使创建和维护 Excel 工作表窗体变得更容易。
引言
我想在我有史以来的第一篇文章中,以一个过度简化的请求作为开篇:不要使用它! 有许多更合适的替代方案(框架、架构、软件等)可以用于此类 UI 和 UX 构建。它应该只用于小型用例,或者当 Excel 确实是您唯一的选择时。
话不多说,我们直接开始……
在处理项目 UI 和 UX 时,我遇到了 3 个主要问题,本框架就是为了解决这些问题而生的。
- 最重要的是,它必须能够实现代码的实际可维护性。
- 它必须在代码和图形方面都能良好扩展。
- 它必须简单易于使用。
总而言之,我认为在这个环境的范围和限制内,这个框架是解决这些问题的绝佳起点。话虽如此,我希望有人能根据自己的需求对其进行改编,并发布任何改进或替代方案,以造福大家。
背景
我非常喜欢 WPF,以及它如何轻松地开发出外观良好的 UI 和 UX。正是这些优点,我最终将这个框架建立在它们之上。
我需要为一位客户创建一个替换的日程表——主要是为了让他们摆脱当时那个糟糕的版本。可以管理员工(每本工作簿只有少数员工)的工作班次以及一些其他统计数据。真正棘手的是,由于他们的安全限制,我只能使用 MS Office。简而言之,我被困在 Excel 和……VBA 中。
在日程表开发早期,我遇到了一个日益严重的问题:维护项目所需的代码呈指数级增长。我不禁想,如果我能使用 WPF,这一切将变得多么容易。如前所述,这不可能。所以我着手,尽我所能地模仿 WPF 的优点——在此过程中重写了大部分代码。
我将工作簿开发到一个可以开始在现场进行测试的程度。就在那时,项目陷入了停滞——距离本文撰写时大约六个月前。一些系统更新(专有的 Windows shell)已经发布,其中包括了更严格的安全措施。正是安全更新导致了项目的夭折——宏启用工作簿不再允许运行。
自项目不幸夭折以来,我计划在这里发布我的工作,希望它能在其他地方蓬勃发展。
使用代码
概述
请探索提供的 Excel 文件及其代码,因为我在这里只介绍一些重点。
按钮
[用扳手敲击]
- 重新启用屏幕绘图。这原本只是一款“开发中”的功能,因为在所有内容开始运行时绘图会被禁用,而且我喜欢在调试时停止它。
[<]
和 [>]
- 循环浏览周数,或者您可以直接覆盖当前数字并按下 Tab 键或 Enter 键。[<]
= 上一周;[>]
= 下一周;
[设置]
- 打开一个用于常规设置和员工维护的窗体。
类
有两个主要类——CellView
和 TemplateView
。第二个类顾名思义,用作模板类,因此它本身并不执行任何操作。我将在文章后面更详细地介绍它的用法。
第一个类 CellView
简化了与单个单元格的交互。它是一个非常轻量级的对象,只包含一个 Range 对象来指向特定单元格,以及一些样板属性访问器。这些访问器使事情变得更容易,我稍后将展示。
为了澄清:对对象之间父子关系的引用(在本文章的范围内)完全是层次意义上的,而不是多态意义上的。
CellView
Private pRg As Range
' Gets/Sets cell this object points to
Public Property Get Pos() As Range
Set Pos = pRg
End Property
Public Property Let Pos(Value As Range)
Set pRg = Value
End Property
Private Function IsInit() As Boolean
IsInit = (Not pRg Is Nothing)
End Function
...
' Value
Public Property Get Value() As String
If IsInit Then
Value = Pos.Value
End If
End Property
Public Property Let Value(val As String)
If IsInit Then
Pos.Value = val
End If
End Property
' Text
Public Property Get Text() As String
If IsInit Then
Text = Pos.Text
End If
End Property
' Address
Public Property Get Address() As String
If IsInit Then
Address = Pos.Address
End If
End Property
示例 - EmpTtlView
以下示例片段能更好地说明此类如何使用。注意:EmpTtlView
是从 TemplateView
复制的,正如前面解释的那样。
'
' EmpTtlView - Handles I/O and formatting for a single employee's daily totals.
'
' <Other Declarations>
...
' Properties
Public TimeWk As CellView
Public GoalWk As CellView
Public SalesWk As CellView
Public OverUnderWk As CellView
Public TimeYTD As CellView
Public GoalYTD As CellView
Public SalesYTD As CellView
Public OverUnderYTD As CellView
Public TimeLbl As CellView
Public GoalLbl As CellView
Public SalesLbl As CellView
Public OverUnderLbl As CellView
...
' Initialization...
Public Function Init(parentView As EmpView)
If Not pInit Then
Set pParentView = parentView
Set pSett = Singletons.GetSettings
pViewWidth = 1
pViewHeight = 1
Set TimeWk = New CellView
Set GoalWk = New CellView
...
'...end Function Init
...
' Function SetPosition...
Set pRg = rgPosition.Cells(1, 1)
' columns
wk = 1
ytd = 2
lbl = 3
'rows
hrs = 1
gls = 2
sls = 3
ou = 4
TimeWk.Pos = pRg.Cells(hrs, wk)
GoalWk.Pos = pRg.Cells(gls, wk)
SalesWk.Pos = pRg.Cells(sls, wk)
OverUnderWk.Pos = pRg.Cells(ou, wk)
TimeYTD.Pos = pRg.Cells(hrs, ytd)
GoalYTD.Pos = pRg.Cells(gls, ytd)
SalesYTD.Pos = pRg.Cells(sls, ytd)
OverUnderYTD.Pos = pRg.Cells(ou, ytd)
TimeLbl.Pos = pRg.Cells(hrs, lbl)
GoalLbl.Pos = pRg.Cells(gls, lbl)
SalesLbl.Pos = pRg.Cells(sls, lbl)
OverUnderLbl.Pos = pRg.Cells(ou, lbl)
' ...end Function SetPosition
...
'----[ Utils ]----------------------------------------------------
Private Function AppendFormula(rgCell As Range, firstStr As String, appendStr As String)
If Not rgCell Is Nothing Then
If rgCell.Formula <> "" Then
rgCell.Formula = Mid(rgCell.Formula, 1, Len(rgCell.Formula) - 1) & ", " & appendStr & ")"
Else
rgCell.Formula = firstStr
End If
End If
End Function
...
' Some simple automated formula creation...
Public Function CalcSales(rgSales As String)
If IsInit Then
AppendFormula SalesWk.Pos, "=SUM(" & rgSales & ")", rgSales
End If
End Function
Public Function CalcOverUnder()
If IsInit Then
OverUnderWk.Formula = "=" & GoalWk.Address & "-" & SalesWk.Address
End If
End Function
...
TemplateView
TemplateView
对象需要一些手动调整,以便尽可能简单地实现层次结构放置和缩放。我将仅介绍 TemplateView
的这一方面,因为它基本上是模板/对象的整个前提。
每个视图都了解并拥有指向其父视图和子视图的引用。我以一种尽可能直观的方式完成了这一切,但肯定有改进的空间。关键
' Replace 'Variant' with actual parent class
Private pParentView As Variant
' Add child(ren) references here...
Private pSett As Settings
' position relative to parent
Private pRg As Range
Private pViewWidth As Integer
Private pViewHeight As Integer
Private pInit As Boolean
...
' TODO: replace 'Variant' with actual parent class
Public Function Init(parentView As Variant)
If Not pInit Then
Set pParentView = parentView
Set pSett = Singletons.GetSettings
pViewWidth = 1
pViewHeight = 1
End If
pInit = True
End Function
'----[ Position ]---------------------------------------------
' SetPosition usage - used from 'pParentView'; usage snippet:
'
' ' array of "day" views (children)
' Private pDayViews(6) As DayView
'
' ' simple day counter
' Dim d As Integer
'
' ' Set position of template view to the first cell of the position arg
' Set pRg = rgPosition.Cells(1, 1)
'
' ' <init/populate pDayViews & other logic>
'
' ' for each day of the week, graphically append each day's view so none overlap
' For d = 0 To 6
' ' <first row>, <next empty/avail column>
' ' <current pos> + <this view's width> + <DayView width * day count>
' pDayViews(d).SetPosition pRg.Cells(1, 1 + pViewWidth + pDayViews(d).OffsetWidth(d))
' Next d
'
' rgPosition is the upper-left most cell of this view
Public Function SetPosition(rgPosition As Range)
If pInit And (Not rgPosition Is Nothing) Then
' Range.Cells(1) lets us not care about merged cells/ranges
Set pRg = rgPosition.Cells(1, 1)
End If
End Function
' Returns this view's width multiplied by offsetIndex.
Public Function OffsetWidth(offsetIndex As Integer) As Integer
If IsInit Then
OffsetWidth = pViewWidth * offsetIndex
End If
End Function
' Returns this view's height multiplied by offsetIndex.
Public Function OffsetHeight(offsetIndex As Integer) As Integer
If IsInit Then
OffsetHeight = pViewHeight * offsetIndex
End If
End Function
这是一个非常有趣的项目,我可能以后还会继续玩/调整它。如上所述,我希望有人能利用它,即使只是其中的一部分。我非常乐意听到任何觉得它有用的人的反馈,或者关于您对自己的副本所做的任何更改。
关注点
回想起来,并且为了更贴近 WPF 类框架,最好是将视图对象与业务逻辑(模型对象)解耦。这样视图只需关心 UI/UX 方面。模型对象将负责所有的思考,让视图负责让事物看起来漂亮。