RSS 2.0 Feed
2006-03 Entries
摘要:这是我第一次发Office开发相关的帖子。说到Office开发,我只能算新手。这次是碰巧开发了一个Excel智能文档项目,其中用到了这个小小的技巧,就发出来让大家看看。在Excel开发中,工作表上最基本也是最常用的元素就是Range,Range可以表达一个获任意多个单元格或者矩形区域的组合,其复杂程度相当高。如果我们的智能文档程序要与用户打交道的话,势必要编程控制文档中的单元格,或与用户选择的单元格交互。而Range的对象模型并不符合.NET开发人员的习惯,要想获取用户选中区域的形状或者操作特定形状的区域都十分繁琐。而MSDN和VSTO的推销人员们只关心诸如怎么把单元格和数据源或者XML绑定之类,这种“小事”只能靠我们自己动手了。我的任务就是编写一个Range的封装类,将Range中所有单元格和矩形区域转化为易于访问的对象模型。首先我们看看Range的组成,一个普通的Range可以是一个或多个矩形区域的集合,每个矩形区域都由一组连续的列和连续的行组成。其中行使用阿拉伯数字索引,而列采用字母索引。如图所示:注意,多个矩形区域可以不连续,还可以交叠。每个Range都有一个描述其位置的字符串,称为Range的地址字符串。地址字符串不但包含所有位置信息,还可以被Excel用来直接快速定位,所以我们就以地址字符串为桥梁,编写我们的包装类。ColumnWrapper类:主要用于吧表示列的字符串“A”,“B”,“AA”等转化为1开始的整数序列,或者相反。我这里用到的算法可以支持无限大的整数与列名字符串互转,但其实Excel只支持到256列。 ColumnWrapperPublic Class ColumnWrapperClass ColumnWrapper    Private Const c_NameChar = " ABCDEFGHIJKLMNOPQRSTUVWXYZ"    Private indexValue As Integer    Public ReadOnly Property Index()Property Index() As Integer        Get            Return indexValue        End Get    End Property    Private nameValue As String    Public ReadOnly Property Name()Property Name() As String        Get            Return nameValue        End Get    End Property    Public Sub New()Sub New(ByVal name As String)        nameValue = name        indexValue = GetIndex(name)    End Sub    Private Function GetName()Function GetName(ByVal i As Integer) As String        Debug.Assert(i > 0)        Dim left As Integer = (i - 1) \ 26        Dim right As Integer = i Mod 26        Dim nameLeft As String        Dim nameRight As String        If left > 0 Then            nameLeft = GetName(left)        Else            nameLeft = String.Empty        End If        If right = 0 Then right = 26        nameRight = c_NameChar(right)        Return nameLeft & nameRight    End Function    Private Function GetIndex()Function GetIndex(ByVal name As String) As Integer        Debug.Assert(Not String.IsNullOrEmpty(name))        Dim len As Integer = name.Length        Dim bit As Integer = 1        For i As Integer = len - 1 To 0 Step -1            Dim c As Char = name(i)            Dim val = c_NameChar.IndexOf(Char.ToUpper(c))            GetIndex += bit * val            bit *= 26        Next    End Function    Public Sub New()Sub New(ByVal index As Integer)        nameValue = GetName(index)        indexValue = index    End Sub    Public Overrides Function GetHashCode()Function GetHashCode() As Integer        Return indexValue    End Function    Public Overrides Function Equals()Function Equals(ByVal obj As Object) As Boolean        If obj Is Nothing Then Return False        Return TryCast(obj, ColumnWrapper).indexValue = indexValue    End FunctionEnd Class有了ColumnWrapper,下面就是CellWrapper,表示单个单元格。 CellWrapperPublic Class CellWrapperClass CellWrapper    Private addr As String    Public ReadOnly Property Address()Property Address() As String        Get            Return addr        End Get    End Property    Private rowValue As Integer    Public ReadOnly Property RowIndex()Property RowIndex() As Integer        Get            Return rowValue        End Get    End Property    Private columnValue As ColumnWrapper    Public ReadOnly Property Column()Property Column() As ColumnWrapper        Get            Return columnValue        End Get    End Property    Private cellValue As String    Public ReadOnly Property Value()Property Value() As String        Get            Return cellValue        End Get    End Property    Public Sub New()Sub New(ByVal row As Integer, ByVal column As ColumnWrapper)        rowValue = row        columnValue = column        Me.addr = "$" & column.Name & "$" & row    End Sub    Private Shared rx As New Regex("\$([A-Z]+)\$([1-9][0-9]*)", RegexOptions.Compiled)    Public Sub New()Sub New(ByVal address As String)        Me.addr = address        Dim matches As MatchCollection = rx.Matches(address)        If matches.Count = 0 Then Throw New ArgumentException("address")        Dim firstMatch As Match = matches(0)        Me.columnValue = New ColumnWrapper(firstMatch.Groups(1).Value)        Me.rowValue = Integer.Parse(firstMatch.Groups(2).Value)    End SubEnd Class接下来我们要表示矩形区域RectRangeWrapper。我们用来表示矩形区域。当我们需要计算矩形区域的大小时,只要使用着两个角单元格的位置信息即可算出。 RectRangeWrapperPublic Class RectRangeWrapperClass RectRangeWrapper    Private addr As String    Public ReadOnly Property Address()Property Address() As String        Get            Return addr        End Get    End Property    Private isWide As Boolean = False    Public ReadOnly Property IsWideRange()Property IsWideRange() As Boolean        Get            Return isWide        End Get    End Property    Private leftTop As CellWrapper    Public ReadOnly Property LeftTopCell()Property LeftTopCell() As CellWrapper        Get            Return leftTop        End Get    End Property    Private rightBottom As CellWrapper    Public ReadOnly Property RightBottomCell()Property RightBottomCell() As CellWrapper        Get            Return rightBottom        End Get    End Property    Public ReadOnly Property Width()Property Width() As Integer        Get            Return Me.rightBottom.Column.Index - Me.leftTop.Column.Index + 1        End Get    End Property    Public ReadOnly Property Height()Property Height() As Integer        Get            Return Me.rightBottom.RowIndex - Me.leftTop.RowIndex + 1        End Get    End Property    Public Sub New()Sub New(ByVal address As String)        If address.IndexOf(":"c) > 0 Then            Dim cells() As String = address.Split(":"c)            Try                leftTop = New CellWrapper(cells(0))                rightBottom = New CellWrapper(cells(1))            Catch ex As ArgumentException                'wide range selected:                leftTop = Nothing                rightBottom = Nothing                Me.isWide = True            End Try        Else            If address Like "$[A-Z,a-z]*$[1-9]*" Then                leftTop = New CellWrapper(address)                rightBottom = leftTop            End If        End If        Me.addr = address    End SubEnd Class最后是多个矩形区域组成的完整Range,我们用MultiRectRangeWrapper类来描述。它其实就是一个矩形区域的集合。 MultiRectRangeWrapperPublic Class MultiRectRangeWrapperClass MultiRectRangeWrapper    Private rectRanges As List(Of RectRangeWrapper)    Public ReadOnly Property Areas()Property Areas() As List(Of RectRangeWrapper)        Get            Return rectRanges        End Get    End Property    Private ReadOnly addr As String    Public ReadOnly Property Address()Property Address() As String        Get            Return addr        End Get    End Property    Public Sub New()Sub New(ByVal address As String)        addr = address        rectRanges = New List(Of RectRangeWrapper)        'parse the ranges        Dim numberOfRanges As Integer        Dim rectRangeAddr As String()        If addr.IndexOf(",") < 0 Then            rectRangeAddr = New String(0) {addr}            numberOfRanges = 1        Else            rectRangeAddr = addr.Split(",")            numberOfRanges = rectRangeAddr.Length        End If        For Each r As String In rectRangeAddr            rectRanges.Add(New RectRangeWrapper(r))        Next    End SubEnd Class好了,现在四个包装类已经全部写完了。用法就简单多了。用Range.Address属性初始化MultiRectRangeWrapper类,就可以得到一个该Range中所有矩形区域的集合,进而轻松访问该举行区域中每个单元格。比如下面这个例子,展示了一些简单的区域操作: '取得用户选定的区域Dim myrange As New MultiRectRangeWrapper(Application.Selection.Address)For Each rectRange As RectRangeWrapper In myrange.Areas    If Not rectRange.IsWideRange Then        '非扩展选择的区域,比如整行或整列        '修改左上角的数字格式        Me.Range(rectRange.LeftTopCell.Address).NumberFormat = "0.00"        '将最后一个选中区域复制到剪贴版        Me.Range(rectRange.Address).Cut()    End IfNext我写这个只是为了我自己写程序方便,并没有考虑太多因素,所以可能写得比较粗糙,功能有限。有兴趣的可以在我这程序的基础上继续改进。 ...[阅读全文]

posted @ | Feedback (9) | Filed Under [ 技术随笔 ]

摘要:本系列未经许可,禁止转载(包括网络媒体刊载) .NET泛型的一大特点是在编译阶段对类型参数不做任何假设。也就是说,面对类型参数T和他的变量,你没有什么能做的——不能调用除Object成员之外的任何方法,不能进行大多数运算符的运算等等。它提供了一个叫约束的机制,能在编译期对类型实参的取值进行一些检查。许多人都将约束视为在类型参数上提供操作支持的唯一方法,并大量使用——你有没有约束过IComparable呢?但是,这种做法是不对的,因为约束仅仅能检测声明类型是否实现了某接口或继承自某类,但通过接口和基类实现的多态机制是一个运行时检查的机制,约束没有从任何方面帮助接口和基类工作。实际上,绝大多数情况下,你用一个约束了IComparable的类型参数T来编程序,和直接用IComparable作为你的操作类型没有什么不同。 约束的真正目的是减少类型实参的取值范围,也就是降低抽象性,是一个泛型具体化的手法。只有当你清楚,你的目的就是“约束”本身,而不是想给类型参数增加些操作的时候,才应该使用约束。那么只想给类型参数增加操作应该怎么办呢?我推荐的方法是:将实现该操作的功能封装到一个外部的辅助类中。比如我们常常想约束IComparable,因为我们想比较类型参数对象的大小。有很多人都用这种方法: [VB]Sub Sort(Of T As IComparable(Of T))(arr As T())[C#]void Sort<T>(T[] arr) where T : IComparable<T> 显而易见,Sort需要数组元素能够比较大小,那么为什么约束不好呢?注意我们约束的是T实现IComparable(Of T),如果T实现的是IComparable呢,就不能比较大小了吗?约束无法表达“实现了IComparable(Of T)或IComparable”这种情况。更进一步,仅有这样的对象才能比较大小吗?有许多类型并没有实现这两个接口中的任何一个,但是仍有比较大小的可能,我们一旦约束就等于将他们拒之门外。因此约束是不适合解决此类问题的。记住:我们唯一需要的就是比较,因此正确的方法是这样: [VB]Sub Sort(Of T)(arr As T(), comparer As IComparer(Of T))[C#]void Sort<T>(T[] arr, IComparer<T> comparer)  注意我们对T现在没有任何限制,但是要求提供一个IComparer(Of T)的实例。这个IComparer(Of T)的实现和T可以没有关系,因此不仅T可以不知晓它的存在,还可以提供不只一种的比较方法。但是这样做,无形要求用户必须提供一个额外的辅助对象,对于一些显而易见可比较大小的对象(如Int32),这种要求显得有些多余。那我们的解决方法就是提供一个默认的比较器: [VB]Sub Sort(Of T)(arr As T())    Sort(arr, Comparable(Of T).Default)End Sub[C#]void Sort<T>(T[] arr){    Sort(arr, Comparable<T>.Default);} 代码中的Comparer(Of T)是System.Collection.Generic下的一个类型,专门用于提供类型默认的比较器。注意到什么了吗?我们现在没有约束了,但是对用户来说,和有约束时的语法一样简单而清晰。比较那些实现了IComparable和IComparable(Of T)的类型时,Comparer(Of T)都提供了支持,而在比较没有实现这类接口的自定义类型时,可自行实现IComparer(Of T)提供比较机制。完美解决。 这种方法还能进行一些约束都实现不了的做法:支持运算符。我们知道在类型参数上实现哪怕最简单的加法都是不允许的,而且没有任何接口可以帮你做到这一点。这时如果能够使用外部辅助类的做法,就能够突破这一恼人的限制,比如VBF就用了下面一个机制来计算类型参数的加法。 [VB]Function Add(Of T)(a As T, b As T, calc As ICalculator(Of T))As T    Return calc.Add(a, b)End FunctionFunction Add(Of T)(a As T, b As T)As T    Return Calculator(Of T).Default.Add(a, b)End Function[C#]T Add<T>(T a, T b, ICalculator<T> calc){    return calc.Add(a, b);}T Add<T>(T a, T b){    return Calculator<T>.Default.Add(a, b);} 现在剩下的问题就是,诸如Comparer(Of T)和Calculator(Of T)这类默认的比较器和计算器是如何实现的?如何保证所实现操作的高效率?这就是我们下一次的任务——类型字典和Type Traits。...[阅读全文]

posted @ | Feedback (26) | Filed Under [ 技术随笔 ]